diff --git a/deno.json b/deno.json new file mode 100644 index 00000000..50207019 --- /dev/null +++ b/deno.json @@ -0,0 +1,5 @@ +{ + "fmt": { + "proseWrap": "never" + } +} diff --git a/docs/README.md b/docs/README.md index 063965fc..ac4c3ba3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,12 +19,16 @@ It is not a dumping ground for external research, old plans, API inventories, or 11. [`design/memory-knowledge.md`](design/memory-knowledge.md) — generated memory and audit records. 12. [`design/workspace-kanban-orchestrator-runtime.md`](design/workspace-kanban-orchestrator-runtime.md) — how Kanban operations become durable orchestration events and backend-internal routing decisions. 13. [`design/workspace-runtime-docker.md`](design/workspace-runtime-docker.md) — the WebUI / Backend / Runtime split, Docker image layout, worker launch path, and workdir materialization boundary. -14. [`development/server-runtime-auth.md`](development/server-runtime-auth.md) — manual Workspace Server / Runtime public-key exchange and authenticated Runtime startup checks. -15. [`development/work-items.md`](development/work-items.md) — how project work is recorded and reviewed. -16. [`development/rust-testing-strategy.md`](development/rust-testing-strategy.md) — what Yoi Rust tests should prove, where they belong, and how to name them. -17. [`development/validation.md`](development/validation.md) — how to check changes. -18. [`development/workspace-schema-migrations.md`](development/workspace-schema-migrations.md) — the canonical SQLite baseline, compatibility fence, and manual dogfooding-data repair procedure. -19. [`design/standalone-agent-host.md`](design/standalone-agent-host.md) — in-process standalone Worker host の依存方向、authority、lifecycle、非目標。 +14. [`development/ui-ux/design-language.md`](development/ui-ux/design-language.md) — Workspace Webの視覚・interaction原則、文法、token、汎用component。 +15. [`development/ui-ux/product-ux.md`](development/ui-ux/product-ux.md) — Workspace resource、navigation IA、resource固有pattern。 +16. [`development/ui-ux/application-architecture.md`](development/ui-ux/application-architecture.md) — application shell、Header、Sidebar、nested override architecture。 +17. [`development/ui-ux/visual-review.md`](development/ui-ux/visual-review.md) — Web UI実装者がbefore/afterを目視し、完了前に残すvisual validation evidence。 +18. [`development/server-runtime-auth.md`](development/server-runtime-auth.md) — manual Workspace Server / Runtime public-key exchange and authenticated Runtime startup checks. +19. [`development/work-items.md`](development/work-items.md) — how project work is recorded and reviewed. +20. [`development/rust-testing-strategy.md`](development/rust-testing-strategy.md) — what Yoi Rust tests should prove, where they belong, and how to name them. +21. [`development/validation.md`](development/validation.md) — how to check changes. +22. [`development/workspace-schema-migrations.md`](development/workspace-schema-migrations.md) — how to preflight, apply, verify, and roll back control-plane SQLite schema changes. +23. [`design/standalone-agent-host.md`](design/standalone-agent-host.md) — in-process standalone Worker host の依存方向、authority、lifecycle、非目標。 ## What belongs here diff --git a/docs/design/workspace-web-design-system.md b/docs/design/workspace-web-design-system.md deleted file mode 100644 index 192c86f9..00000000 --- a/docs/design/workspace-web-design-system.md +++ /dev/null @@ -1,78 +0,0 @@ -# Workspace web design system - -This document defines the visual rules for `web/workspace`. The current authority for tokens and reusable page/sidebar styling is `web/workspace/src/app.css`. - -## Design position - -Workspace web should read as a control surface, not a set of detached widgets. Group information primarily through spacing, typography, and text contrast. Use borders, rounded rectangles, shadows, and filled panels only when they clarify hierarchy that spacing cannot express. - -## Palette - -Colors are defined as CSS custom properties in OKLCH. The palette supports light and dark modes through `prefers-color-scheme`. - -Rules: - -- Background and layout surfaces use zero chroma: `oklch(... 0 0)`. -- Primary text and code text are near-neutral warm colors. Because CSS OKLCH exposes chroma (`C`) rather than saturation directly, encode the “about 5% saturation” intent as very low warm chroma, around `C = 0.01` to `0.012`. -- Muted text reduces lightness/chroma before introducing new hues. -- Accent/status colors are semantic exceptions. They should mark state, focus, or navigation, not decorate containers. -- Do not introduce raw hex/rgb colors in Workspace web components. Add or reuse a token in `app.css`. - -Core tokens: - -```css ---bg ---bg-raised ---bg-subtle ---line ---line-strong ---text ---text-strong ---text-muted ---text-faint ---code ---accent ---success ---warning ---danger -``` - -## Layout and grouping - -Prefer vertical rhythm and text hierarchy over card chrome. - -- Page sections are separated by whitespace and a light top rule. -- Navigation selection uses a left rule rather than filled pills. -- Nested records use indentation or top rules, not repeated rounded containers. -- Shadows are avoided in the base system. -- Rounded corners are reserved for small controls where hit area shape matters. - -## Typography - -- Headings and primary labels use `--text-strong`. -- Body text uses `--text`. -- Metadata, helper text, timestamps, and table headings use `--text-muted` or `--text-faint`. -- Uppercase labels are acceptable for small metadata labels only; avoid large all-caps UI blocks. - -## Component styling boundary - -`app.css` owns the shared visual language: - -- reset/base body styles -- OKLCH tokens -- layout primitives -- page cards/sections -- sidebar/navigation sections -- tables, kanban lists, diagnostics, record bodies - -Svelte components should keep local styles only when a behavior is truly component-specific. If a style affects color, spacing, borders, text hierarchy, or repeated record layout, it belongs in `app.css`. - -## Adding new UI - -When adding Workspace web UI: - -1. Start with semantic HTML and existing classes from `app.css`. -2. Use spacing and text contrast first. -3. Use a border only when the boundary carries meaning. -4. Use background fills only for page-level surfaces or read-only code/record bodies. -5. If a new color is needed, define it as an OKLCH token and document why the existing semantic tokens are insufficient. diff --git a/docs/development/ui-ux/application-architecture.md b/docs/development/ui-ux/application-architecture.md new file mode 100644 index 00000000..c5312c1d --- /dev/null +++ b/docs/development/ui-ux/application-architecture.md @@ -0,0 +1,104 @@ +# Workspace Web Application Architecture + +この文書は、Workspace Webのapplication shell、Header、Sidebar、nested +layoutのcomposition authorityを定義する。Navigation +IAは[`product-ux.md`](product-ux.md)、視覚とinteractionの共通規則は[`design-language.md`](design-language.md)、具体的なsource +pathとCSS +ownershipは[`../../../web/workspace/README.md`](../../../web/workspace/README.md)を参照する。 + +## Application shell + +root layoutが唯一のapplication shellを所有する。 + +- Sidebar frame +- global fallback navigation +- Header frame +- main content +- SidebarとHeaderのoverride context + +page routeはapplication shell、global Header、Sidebar、fold +controlを再実装しない。Pageのmarkupは標準shellのmainへ配置するcontentから始める。 + +## Header composition + +Headerはroot shellが一度だけrenderする。Nested +layoutは`HeaderOverride`を通じて現在scopeのlocation contentを登録する。 + +- page componentはHeader frameを作らない。 +- 最も深いactive overrideを表示する。 +- layoutが破棄されたら直前のoverrideへ戻る。 +- overrideがなければroot fallbackを表示する。 +- route titleをmain contentへ複製しない。 + +## Sidebar composition + +### Root + +root shellは`SidebarFrame`と`GlobalSidebar`を一度だけrenderし、root +`SidebarController`を提供する。Overrideがない場合、`GlobalNavSections`がfallbackになる。 + +### Workspace + +Workspace layoutは、親のroot +slotへ`WorkspaceSidebar`を登録する。同時に、さらに深いlayoutが使う新しい`SidebarController`をcontextへ設定する。 + +`WorkspaceSidebar`はWorkspace shortcut headerを保持し、その下へchild +slotをrenderする。Child overrideがない場合だけ、Workspace navigation +fallbackを表示する。 + +### Settings + +Settings layoutは、Workspace layoutが提供したchild +slotへ`SettingsSidebar`を登録する。Workspace shortcut headerは残り、Workspace +navigation fallbackだけがSettings navigationへ置き換わる。 + +さらに深いscopeも同じcontractを使う。 + +## Override stack + +`HeaderOverride`と`SidebarOverride`は、layout lifecycleにbindしたLIFO +stackとして扱う。 + +1. Layout mount時にparent controllerへregisterする。 +2. Controllerは最も新しいactive registrationを表示する。 +3. Nested layoutはchild controllerを新しく提供する。 +4. Layout destroy時に自分のregistrationをremoveする。 +5. 一つ前のregistrationがあれば復元する。 +6. Stackが空ならfallbackへ戻る。 + +同じregistrationを複数回removeしても結果を変えない。古いcleanupが新しいactive +registrationを削除してはならない。 + +## Ownership + +- `SidebarFrame`だけが`aside`、外枠、scroll領域、fold controlを所有する。 +- `GlobalSidebar`、`WorkspaceSidebar`、`SettingsSidebar`は各scopeのcontentを所有する。 +- `SidebarOverride`を登録できるのはscopeを定義するlayoutだけとする。 +- page component、一時的なwidget、dialogはSidebarを差し替えない。 +- child layoutは親navigationをcopyせず、自分のsidebar layerだけをregisterする。 +- Global、Workspace、Settingsを一つのsnippetへ平坦化しない。 + +## Data authority + +Sidebar compositionはnavigation +structureを決めるが、Workspace、permission、Worker +stateのauthorityにはならない。 + +- Workspace identityとpermissionはBackend projectionを使う。 +- Worker listとstateはWorkspace protocol projectionを使う。 +- route pathやdisplay labelからauthorityを推測しない。 +- unavailable dataを架空の正常stateで置き換えない。 +- permissionにより利用できないnavigationは、権威あるpermission + projectionに基づいて除外する。 + +## Design-lab + +認証不要のstatic design-labはroot shellを再利用し、実際と同じnested +controllerとoverride stackを構成する。 + +Live Workspace protocolへbindするresource listだけは、同じscope layer内のstatic +fixtureへ置き換えてよい。Static fixtureへ別scopeのnavigationを混ぜず、production +componentと同じ順序、state表現を使う。 + +Design-lab固有のswitch controlでSidebarを差し替えず、route +hierarchyによってlayout lifecycleを発生させる。 diff --git a/docs/development/ui-ux/design-language.md b/docs/development/ui-ux/design-language.md new file mode 100644 index 00000000..bb70169e --- /dev/null +++ b/docs/development/ui-ux/design-language.md @@ -0,0 +1,196 @@ +# Workspace Web Design Language + +この文書は、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とする。具体的な二案の一方を棄却できない文は規則として残さない。 + +## 1. Principles + +複数の規則や要求が競合した場合だけ、この章の優先原理を使う。 + +### Task before explanation + +次の有効なactionへ到達する前の操作、scroll、常設説明が少ない案を選ぶ。ただし、説明を省くと対象、結果、permission、不可逆性を誤認する場合は説明をactionより前に置く。 + +### Meaning before decoration + +情報の関係、重要度、操作可能性、stateのいずれも変えない視覚差を追加しない。 + +### Authority before convenience + +Backend authorityが確定していないdata、state、permission、actionを、UIの都合で補完または推測する案を棄却する。 + +## 2. Semantics / Grammar + +### Information structure + +- 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を変える値だけを常設する。 + +**Disclosure** + +- Raw dataやデバッグ情報など、通常は必要としない詳細な情報は折りたたみ領域に隠す。 +- 作成または編集のformを表示用screenへ常時同居させず、明示的なactionで開く折りたたみ領域または別pageに置く。 + +### Interaction and state + +- actionにはbutton、navigationにはlink、選択には対応するform controlを使う。 +- interactive elementはhover、focus、active、disabledを視覚的に区別する。 +- staticなstatus、key、metadataをbuttonやpillの形にしない。 +- clickable rowは遷移先を一つだけ持ち、row全体で同じ遷移を実行する。 +- permissionがないactionをdisabled controlとして表示しない。 + +**State and 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を要求する。 + +### Language / Copy + +- title、navigation label、control labelで明らかな内容を説明文で繰り返さない。 +- 常設proseは、対象またはactionの選択を変えるwarning、permission、validation constraint、error recoveryに必要な内容だけにする。 +- button labelには具体的なactionを表す動詞を使い、`Submit`と`OK`を使わない。 +- identifier、API名、type名を別の語へ言い換えない。 + +### Responsive behavior + +- page全体にhorizontal scrollを発生させない。 +- wrappingまたはstack化によって情報の対応関係が失われる場合だけ、その領域内にbounded horizontal scrollを持たせる。それ以外のside-by-side layoutは一列にする。 +- primary actionを初期viewportから追い出さない。 +- contentをtruncateする場合、完全な値をcopyまたはdetailで取得できるようにする。 + +### Accessibility + +- action、navigation、form controlを非semantic elementだけで実装しない。 +- すべてのcontrolにaccessible nameを与える。 +- keyboard focusを不可視にしない。 +- tab orderを画面上の順序と一致させる。 +- stateをcolorだけで区別しない。 +- horizontal scroll regionをkeyboard focus可能にし、accessible nameを与える。 + +## 3. Primitives / Tokens + +### Color / Theme + +colorはCSS custom propertyをauthorityとし、通常のWorkspace colorはOKLCHで定義する。lightとdarkは同じsemantic token名を使う。 + +```css +--bg +--bg-raised +--bg-subtle +--line +--line-strong +--text +--text-strong +--text-muted +--text-faint +--code +--accent +--accent-muted +--success +--warning +--danger +--interactive-hover +--interactive-selected +--shadow-overlay +``` + +- `--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 + +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-title /* 24px */ +--line-height-title /* 32px */ +--font-size-body /* 14px */ +--line-height-body /* 20px */ +--font-size-compact /* 12px */ +--line-height-compact /* 16px */ +``` + +### Spacing / Shape + +```css +--space-1 /* 4px */ +--space-2 /* 8px */ +--space-3 /* 12px */ +--space-4 /* 16px */ +--space-5 /* 24px */ +--space-6 /* 32px */ +--radius-soft +``` + +- 一つの境界にspacing、line、surface backgroundを重ねず、primary cueを一つだけ使う。 +- 汎用的な`Card` primitiveを設けない。 +- nested surfaceでparentと同じ境界表現を繰り返さない。 +- shadowは通常flowから浮くmenu、popover、Tooltipだけに使う。 +- radiusはinteractive controlだけに使う。 + +### Motion + +- motion完了をactionの受付、state change、content理解の条件にしない。 +- 繰り返しまたは連続animationを、`prefers-reduced-motion: reduce`で停止する。 + +## 4. Components / Patterns + +### Border / Structural edge + +- `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 + +- 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に入れる。 +- pointer hoverとkeyboard focusの両方で開く。 +- triggerとTooltipを`aria-describedby`で関連付ける。 +- hoverはdelay後、focusは直ちに表示する。 +- `Escape`で閉じ、triggerからfocusを移動しない。 +- 複数paragraph、form、link、buttonを入れない。 +- error、permission、validation constraint、不可逆操作の結果、current stateをTooltipへ入れない。 +- touch環境ではTooltipだけを説明への到達手段にしない。 + +### Generic component patterns + +- **Form field**: label、control、constraintまたはhelp、field errorの順に配置する。 +- **Key-value**: 一つのresourceの属性には`dl`を使う。 + +Design-labではcomponent名と実際のsampleだけを表示する。規則とrationaleをUIへ書かない。 diff --git a/docs/development/ui-ux/product-ux.md b/docs/development/ui-ux/product-ux.md new file mode 100644 index 00000000..3420a646 --- /dev/null +++ b/docs/development/ui-ux/product-ux.md @@ -0,0 +1,143 @@ +# Workspace Web Product UX + +この文書は、Workspace Webで扱うresource、navigationのinformation +architecture、resourceごとの主要taskを定義する。見せ方の共通文法は[`design-language.md`](design-language.md)、application +shellとSidebar +slotは[`application-architecture.md`](application-architecture.md)を参照する。 + +## Product position + +Workspace +Webは、Workspace内の権威ある状態を確認し、注意が必要な箇所を判断し、一度に一つの操作を行うための作業面である。Marketing +dashboard、analytics画面、Backend DTOのraw viewerにはしない。 + +基本的な操作の流れは次とする。 + +```text +scopeを選ぶ +→ resourceを選ぶ +→ current stateを確認する +→ 次の有効なactionを行う +→ 結果を確認する +``` + +## Resource + +主要resourceは次のとおり。 + +- Workspace +- Ticket +- Objective +- Merge Request +- Memory +- Worker +- Workdir +- Runtime +- Repository + +通常の表示とURLにはcanonical resource keyを使う。内部UUIDをlabelやfallback +linkとして露出しない。 + +## Navigation IA + +### Global + +Workspaceを選ぶ前のscopeを扱う。 + +- Workspace catalog +- Account +- Device Login +- Workspace作成 + +### Workspace + +一つのWorkspace内で日常的に扱うresourceを置く。 + +1. Tickets +2. Objectives +3. Merge Requests +4. Memory + - Document + - Staging +5. Workers + +### Settings + +Workspaceの管理と構成を扱う。 + +- Runtimes +- Configuration Sources +- Repositories +- Repository Access +- Profile Sources +- Workspace Identity + +`Runtimes`、`Repositories`、`Repository Access`をWorkspace primary +navigationへ混ぜない。permissionにより利用できないSettings itemは表示しない。 + +## Page model + +各routeは、主要な目的を一つだけ持つ。その目的は「Ticketを選ぶ」「このWorkerを確認する」「Repository +accessを設定する」のように、動詞と対象を一つずつ使って表現できなければならない。 + +Workspace pageは、次のいずれかを基本形とする。 + +1. **一覧またはboard** — resourceを探し、注意が必要な項目を見つける。 +2. **詳細** — 一つのresourceを理解し、現在実行できる次の操作を行う。 +3. **formまたはeditor** — 範囲の明確な設定変更を行う。 +4. **Console** — 一つのWorkerを操作しながら、履歴とcurrent controlを確認する。 + +同じ目的を支える場合に限り、一つのrouteで複数の基本形を組み合わせてよい。 + +## Product pattern + +### Tickets + +- boardを一つの横方向の作業面として扱う。 +- laneはworkflow stateを表し、隣接laneとの関係を維持する。 +- Ticketはlane内でDesign Languageの`Resource row` patternを使う。 +- Ticket key、title、優先判断を変えるmetadataだけを表示する。 +- blockerとattentionはtextでも示す。 +- narrow viewportでは、bounded horizontal scrollでlane比較を維持する。 + +### Resource index + +- 同じfieldをresource間で比較する場合、flat listまたはtableを使う。 +- 人が読むnameとcanonical keyを最初のcolumnに置く。 +- columnは判断に必要な値だけに絞る。 +- provider detail、revision、digestはdetailまたはtechnical disclosureへ移す。 + +### Resource detail + +- identity、current state、attentionを最初に置く。 +- narrativeまたはhistoryをprimary領域に置く。 +- relation、target、review、workflowが別のauthorityを持つ場合、それぞれを独立したsectionにする。 +- raw metadataはtechnical disclosureへ置く。 + +### Settingsとform + +- 一つのSettings routeは一つの設定関心事を所有する。 +- edit actionの前または同時に、現在有効な値を示す。 +- 一つの判断に必要なfieldをgroup化する。 +- narrow viewportでは一列にする。 +- form内でfilled actionにするのはsave actionだけとする。 + +### Worker Console + +- transcriptをprimary surfaceとする。 +- direct SubWorkerを表示している場合も、Composerとrun + controlは親Workerへbindしたままにする。 +- OverviewとNormalはpresentationだけを変え、authorityを変えない。 +- command outputはterminalに近いcompactな表示とし、prose messageから分離する。 +- live stateは権威あるWorker snapshotから導出する。 +- connection stateやtransient eventでcurrent stateを置き換えない。 +- hidden reasoningとraw system promptはdebug modeでも表示しない。 + +## Stateとpermission + +- 存在しない、閲覧できない、読み込みに失敗したWorkspaceを別の状態として扱う。 +- 別のWorkspaceへ暗黙に切り替えない。 +- owner-only mutation controlをnon-ownerへ表示しない。 +- read-onlyの場合は、有用なread viewを残す。 +- Workerのdomain stateとWebSocket connection stateを混同しない。 +- Mergeなど不可逆または重要なoperationは、対象と結果を明示してから実行する。 diff --git a/docs/development/ui-ux/visual-review.md b/docs/development/ui-ux/visual-review.md new file mode 100644 index 00000000..25b28646 --- /dev/null +++ b/docs/development/ui-ux/visual-review.md @@ -0,0 +1,272 @@ +# Web visual review + +この文書は、Web UIを変更する実装者が完了前に行うvisual +reviewの必須手順を定義する。Visual reviewは任意のpolish工程ではなく、type +check、linter、testと並ぶvalidationである。 + +[`design-language.md`](design-language.md)はWorkspace +Webが満たす視覚とinteractionの共通規則を定義する。Product固有の構成は[`product-ux.md`](product-ux.md)を参照する。[`tools/web-ux/README.md`](../../../tools/web-ux/README.md)はcapture +toolの操作方法を定義する。この文書は、誰が、いつ、何を目視し、どの証拠を残さなければ実装完了とみなせないかを定義する。 + +## 実装者の責任 + +Visual +reviewは、後段のReviewerやユーザーへ最初の発見を委ねない。UIを変更した実装者自身が、同じ変更の中で次を行う。 + +1. 変更前をcaptureする。 +2. 変更後を同じ条件でcaptureする。 +3. screenshotとbrowser evidenceを自分で確認する。 +4. 発見した問題を修正する。 +5. 修正後を再captureして確認する。 +6. 変更と無関係な既存問題は、根拠を付けて分離する。 + +Agentが実装者の場合、そのAgent自身が`ViewImage`または同等のVLM入力で実画面を確認する。captureを生成しただけ、別のAgentへ画像を渡しただけ、HTTP +200を確認しただけでは完了しない。 + +## 適用範囲 + +次の変更ではvisual reviewを必須とする。 + +- page、layout、navigation、Sidebar、Header、Console、dialog +- componentのmarkupまたはstyle +- color、typography、spacing、border、radius、shadow、theme token +- copy、label、status、empty/loading/error/permission表示 +- responsive breakpoint、container、overflow、scroll +- 表示dataの追加、削除、並び替え、progressive disclosure +- icon、focus、keyboard interaction、motion +- UIへ影響するgenerated typeまたはAPI projection + +型、Backend、CLI、文書だけの変更でrendered Web +UIが変わらない場合は省略できる。省略する場合は、UIへ影響しない理由をvalidation +evidenceへ一文で残す。 + +## 必須の確認条件 + +変更したsurfaceに関係する条件だけを選ぶが、都合のよい一画面だけに限定しない。 + +### Persona + +権限やdataが表示を変える場合は、該当するpersonaを分ける。 + +- owner +- non-owner +- anonymous +- read-onlyまたは操作権限なし + +owner表示だけでpermission-aware UIを承認しない。 + +### Viewport + +最低限、次を確認する。 + +- desktop: `1440px`幅を基準 +- narrow: Sidebarとmainが競合する幅。現在は`768px`を基準 +- mobile: `390px`幅を基準 + +`320px`対応、table、long form、Console、Sidebar +foldなどが変更対象なら、その境界も追加する。media +queryのviewport幅だけでなく、Sidebarやpanelを差し引いた実際のcontent幅を確認する。 + +### Theme + +color、border、background、focus、status、code、terminalを変更した場合はlightとdarkの両方を確認する。片方のthemeで読めることを、もう片方のcontrastの証拠にしない。 + +### Data state + +該当するstateを明示的に用意する。 + +- representative data +- empty +- loading +- error +- permission deniedまたはread-only +- long title、long key、long error、複数行content +- collectionの最小件数と、実際に起こりうる多い件数 + +happy pathだけでcomponentを承認しない。 + +## 目視確認の観点 + +### 情報のまとまり + +- 同じ判断に使う情報がproximity、alignment、typographyによって一つのgroupに見えるか。 +- 別の意味を持つ情報が、十分な距離または一つの明確なboundaryで分離されているか。 +- 親子、peer、metadata、actionの関係が、DOMを読まなくても判別できるか。 +- card、background、border、shadow、余白を重ねて同じgroupingを重複表現していないか。 +- page title、Header、breadcrumb、section + heading、説明文が同じ意味を繰り返していないか。 + +### Separatorとborder + +画面上のすべての線について、何と何を区切る線か説明できなければならない。 + +- Headerの終端、peer row間、section開始、table + cellなど、線の役割が一つに定まっているか。 +- 近接した二本の線が同じboundaryを重複表現していないか。 +- 線の上下の余白から、その線が前後どちらのgroupに属するか分かるか。 +- すべてのsectionへ機械的に同じ線を置き、hierarchyを平坦化していないか。 +- borderがなくてもspacingだけで十分な箇所へ線を追加していないか。 + +### Spacingとdensity + +- 余白がtoken scaleに沿い、近い関係ほど狭く、別groupほど広くなっているか。 +- labelとvalue、headingとcontent、rowとrow、sectionとsectionの間に一貫した大小関係があるか。 +- 線の前後へ同程度の大きな余白を置き、boundaryの所属を曖昧にしていないか。 +- 初期viewportにprimary contentまたは主要操作が存在するか。 +- 空きすぎた領域が情報不足やhierarchyの弱さを隠していないか。 +- 密度を上げるためにfont、touch target、line heightを過度に縮めていないか。 + +### Wrapping、overflow、可変長content + +- 実際に起こりうる最長のlabel、title、key、ref、error、翻訳で確認したか。 +- 一行を前提とするcontrolが意図せず二行にならないか。 +- button labelが折り返されたとき、row heightとalignmentが壊れないか。 +- title、metadata、code、URLが隣接actionを押し出さないか。 +- truncateする場合、完全な値へaccessできるか。 +- horizontal + scrollはtable、board、terminalなど、その意味を所有する領域だけに閉じているか。 +- page全体へhorizontal overflowが発生していないか。 +- `overflow-wrap`だけで問題を隠さず、可読性と比較可能性を維持しているか。 + +### Responsive layout + +- DOM順、視覚順、keyboard順が一致しているか。 +- Sidebar、Header、primary contentの順序がmobileでも目的に合うか。 +- Sidebarだけで初期viewportを占有していないか。 +- breakpoint直前と直後の両方で、content幅とcontrol配置が成立するか。 +- viewport + queryだけでなく、containerの実幅に応じて一列化またはscrollを選べているか。 +- desktopの比較関係を、理由なくmobile cardへ変換していないか。 + +### Typographyとcopy + +- route titleが画面上で一度だけ表示されているか。 +- page purpose、componentの存在理由、設計意図を説明するmeta + copyが残っていないか。 +- proseが判断、warning、constraint、permission、recoveryのいずれかに実際に必要か。 +- heading level、font size、weight、colorが同じhierarchyで揃っているか。 +- metadataがbodyより強く見えていないか。 +- uppercase、letter spacing、monospaceを装飾目的で多用していないか。 + +### State、permission、action + +- loading、empty、error、permission denied、unavailableが見分けられるか。 +- statusがcolorだけに依存していないか。 +- 利用できないowner-only actionをdisabledで並べるのではなく、page + compositionから除外できているか。 +- filled primary actionが同じscopeに複数ないか。 +- destructive actionが通常のprimary pathと誤認されないか。 +- errorとrecovery actionが、失敗したsurfaceの近くにあるか。 + +### Interactionとaccessibility + +- keyboard focusが全controlで見えるか。 +- focus順が画面の作業順と一致するか。 +- fold、disclosure、dialog、menuを操作した後にfocusが失われないか。 +- hoverでしか得られない必須情報がないか。 +- scroll region、icon-only control、status markerにaccessible + nameまたはtextがあるか。 +- reduced motionで意味が失われないか。 + +## 実行手順 + +### 1. Scenarioを決める + +変更対象route、persona、viewport、theme、data state、capture +pointを列挙する。既存scenarioが目的を満たす場合は再利用し、満たさない場合は同じ変更内で追加または更新する。 + +### 2. 変更前をcaptureする + +`tools/web-ux`を使い、変更前のsourceとscenarioを固定する。出力はrepositoryのproduction +sourceへ混ぜず、`target/web-ux`などの開発artifact領域へ置く。 + +### 3. 変更前を目視する + +既存問題も含め、変更対象付近のgrouping、separator、spacing、wrapping、responsive、stateを確認する。変更前を見ずに、変更後の印象だけで改善を主張しない。 + +### 4. 実装する + +発見した問題と、適用するUX規則を対応付けて修正する。screenshotだけを整えるfixture固有hackや、productionと異なるshell、CSS、data +authorityを作らない。 + +### 5. 同条件で再captureする + +persona、route、viewport、theme、data +stateを揃える。変更後だけ別の好条件へ変えない。 + +### 6. Screenshotとreview contextを確認する + +実装者自身が画像を開き、`review-context.json`も読む。次を確認する。 + +- document status +- visible UI error +- console error +- page error +- failed request +- accessibility snapshot +- screenshot hashとcapture point + +HTTP 200でも画面に`401 Unauthorized`などが表示されていれば失敗とする。 + +### 7. Compareする + +before/afterを比較し、差分が意図した領域に限定されているか確認する。pixel +differenceの大小だけで合否を決めない。意図しない移動、折返し、欠落、色変化を目視する。 + +### 8. Testとevidenceを揃える + +source test、component test、E2E、type checkを実行し、visual +evidenceと一緒に記録する。test成功はvisual reviewの代替ではなく、visual +review成功もtestの代替ではない。 + +## 合格条件 + +次をすべて満たした場合だけvisual reviewを合格とする。 + +- 実装者自身がbefore/afterを目視した。 +- 必要なpersona、viewport、theme、data stateを確認した。 +- grouping、separator、spacing、wrapping、responsiveを確認した。 +- visible、console、page、request errorが解決または明確にdispositionされた。 +- 発見した新規問題を修正し、修正後を再captureした。 +- 意図しないvisual regressionがない。 +- artifact path、scenario、確認結果をhandoffへ記録した。 + +次は不合格である。 + +- capture commandが成功しただけ。 +- screenshotを生成したが開いていない。 +- desktopだけ、ownerだけ、happy pathだけを見た。 +- 後段Reviewerまたはユーザーが目視する前提でhandoffした。 +- 「好みの問題」として、説明できないseparator、spacing、overflowを未確認のまま残した。 +- testまたはlinterが成功したため、visual reviewを省略した。 + +## Evidenceの記録 + +handoffには最低限、次を記録する。 + +```text +Visual review +- Scenario: +- Before: +- After: +- Personas: +- Viewports: +- Themes: +- Data states: +- Findings fixed: +- Known issues: +- Result: pass | fail +``` + +画像をTicketやGitへ大量にcommitしない。artifact pathとsource +revisionを結び付け、必要なreviewerが同じ条件を再現できるようにする。認証profile、cookie、token、secret、private +response bodyをartifactやhandoffへ含めない。 + +## Toolの位置付け + +`tools/web-ux`はbrowser起動、scenario再現、capture、review +context、compareを提供する。Toolは視覚的な良否を自動決定しない。 + +Linterが構文規則を検査し、testが特定のbehaviorを検査するのと同様に、visual +reviewはrendered +resultの関係性を検査する。三者は補完関係であり、どれか一つで他を代替しない。 diff --git a/docs/development/validation.md b/docs/development/validation.md index 810d58e8..72cc360c 100644 --- a/docs/development/validation.md +++ b/docs/development/validation.md @@ -32,6 +32,20 @@ cargo check --workspace Avoid repository-wide formatting churn when a validation failure is caused by pre-existing unrelated formatting. +## Web UI changes + +Rendered Web UI changes require implementer-owned visual review in addition to type check, linter, and test. Follow [`ui-ux/visual-review.md`](ui-ux/visual-review.md). + +Minimum evidence includes: + +- before/after capture under the relevant persona, viewport, theme, and data state; +- the implementer’s own inspection of grouping, separator meaning, spacing, wrapping, overflow, responsive order, and visible errors; +- `review-context.json` inspection; +- disposition of every observed problem; +- artifact paths and a final `pass | fail` result. + +A successful capture command, HTTP 200, screenshot generation, or later Reviewer inspection does not replace this validation. + ## Work item checks Run: diff --git a/tools/web-ux/README.md b/tools/web-ux/README.md index e78bab0c..63b7be79 100644 --- a/tools/web-ux/README.md +++ b/tools/web-ux/README.md @@ -4,6 +4,10 @@ real Web Workspace. It does not add a Yoi product Skill, Flow, Runtime capability, or browser automation route. +The required implementer-owned review workflow and pass/fail policy are defined in +[`../../docs/development/ui-ux/visual-review.md`](../../docs/development/ui-ux/visual-review.md). This +README documents the tool; running the tool is not itself visual review completion. + The workbench produces a **review context bundle** rather than treating a screenshot as evidence by itself. Every capture records the persona, route, viewport, theme, intended user goal, expected data state, sanitized document URL/status, console/page/request failures, screenshot hashes, an diff --git a/tools/web-ux/deno.json b/tools/web-ux/deno.json index 7bf2388b..172a00eb 100644 --- a/tools/web-ux/deno.json +++ b/tools/web-ux/deno.json @@ -14,6 +14,7 @@ "test:browser": "deno test --allow-env --allow-net --allow-read --allow-write --allow-run --allow-sys browser-tests/capture_smoke_test.ts" }, "fmt": { - "lineWidth": 100 + "lineWidth": 100, + "proseWrap": "never" } } diff --git a/web/workspace/README.md b/web/workspace/README.md index 91957522..b54b4ecf 100644 --- a/web/workspace/README.md +++ b/web/workspace/README.md @@ -55,3 +55,48 @@ the Vite dev server proxy plus the Rust backend command above. deno task check deno task build ``` + +## Design and architecture authority + +- [`../../docs/development/ui-ux/design-language.md`](../../docs/development/ui-ux/design-language.md): visual and interaction language. +- [`../../docs/development/ui-ux/product-ux.md`](../../docs/development/ui-ux/product-ux.md): resource and navigation IA. +- [`../../docs/development/ui-ux/application-architecture.md`](../../docs/development/ui-ux/application-architecture.md): shell and nested override architecture. +- [`../../docs/development/ui-ux/visual-review.md`](../../docs/development/ui-ux/visual-review.md): implementer-owned visual validation. + +This README owns frontend implementation guidance and concrete source paths. It does not redefine product or visual rules from those documents. + +## CSS ownership + +`src/app.css` is the global foundation and owns only: + +- font imports; +- cascade layer order; +- semantic tokens and light/dark theme; +- reset and base element typography; +- global focus and text-selection behavior. + +It must not own page layout, feature class selectors, table or definition-list layout, button variants, status presentation, helper spacing, or anchor color. Navigation, inline links, and action links own their color in the relevant component stylesheet. + +Feature styles are owned by their implementation area: + +- `src/lib/workspace/styles/workspace-pages.css`: Workspace main-content primitives. +- `src/lib/workspace/styles/tickets.css`: Ticket surfaces. +- `src/lib/workspace/styles/workers.css`: Worker surfaces. +- `src/lib/workspace/styles/settings.css`: Settings, Account, and form surfaces. +- `src/lib/workspace/styles/workspace-catalog.css`: Workspace catalog. +- `src/lib/workspace/sidebar/sidebar.css`: Sidebar components. +- Svelte-local styles: behavior-specific presentation that is not reused outside that component. + +Use semantic tokens from `app.css`. Do not introduce component-local color, spacing, font, or z-index systems. + +## Source map + +- `src/routes/+layout.svelte`: root application shell and override contexts. +- `src/routes/w/[workspaceId]/+layout.svelte`: Workspace Header and Sidebar registration. +- `src/routes/w/[workspaceId]/settings/+layout.svelte`: Settings Sidebar registration. +- `src/lib/workspace/header/`: Header frame, context, and overrides. +- `src/lib/workspace/sidebar/`: Sidebar frame, scoped content, contexts, and override stack. +- `src/lib/workspace/ui/`: generic presentation components such as `Tooltip`, `Bevel`, and `BevelLine`. +- `src/routes/design-lab/workspace-web-ux/`: static Design Language showroom. + +When one of these paths changes, update this source map in the same change. Stable architecture and UX authority remain in `docs/`; this map follows the current implementation. diff --git a/web/workspace/deno.json b/web/workspace/deno.json index 0887fc85..f5c64dd8 100644 --- a/web/workspace/deno.json +++ b/web/workspace/deno.json @@ -6,7 +6,7 @@ "dev": "deno run -A npm:vite@7.2.7 dev", "dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787", "check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json", - "test": "deno test --allow-read=src,test,tests --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts test/repository-ssh-connection-ui.test.ts", + "test": "deno test --allow-read=src,test,tests,../../docs --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts tests/tooltip-contract.test.ts tests/bevel-contract.test.ts tests/bevel-line-contract.test.ts tests/typography-contract.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts src/lib/workspace/console/composer-delivery.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts test/repository-ssh-connection-ui.test.ts", "build": "deno run -A npm:vite@7.2.7 build", "preview": "deno run -A npm:vite@7.2.7 preview" }, @@ -32,5 +32,8 @@ "svelte-check": "npm:svelte-check@4.3.4", "typescript": "npm:typescript@5.9.3", "vite": "npm:vite@7.2.7" + }, + "fmt": { + "proseWrap": "never" } } diff --git a/web/workspace/src/app.css b/web/workspace/src/app.css index 4192dc8a..d2f1d833 100644 --- a/web/workspace/src/app.css +++ b/web/workspace/src/app.css @@ -6,16 +6,20 @@ @import "@fontsource/ibm-plex-mono/latin-500.css"; @import "@fontsource/ibm-plex-mono/latin-600.css"; @import "@fontsource/ibm-plex-mono/latin-700.css"; + @layer reset, tokens, base, layout, components; + @layer reset { *, *::before, *::after { box-sizing: border-box; } + body { margin: 0; } + button, input, textarea, @@ -23,9 +27,11 @@ font: inherit; } } + @layer tokens { :root { color-scheme: light dark; + --bg: oklch(98.5% 0 0); --bg-raised: oklch(96% 0 0); --bg-subtle: oklch(93% 0 0); @@ -41,6 +47,28 @@ --success: oklch(48% 0.11 145); --warning: oklch(62% 0.12 85); --danger: oklch(54% 0.14 25); + --interactive-hover: oklch(94.5% 0 0); + --interactive-selected: oklch(93% 0.02 230); + --shadow-overlay: 0 12px 32px oklch(0% 0 0 / 0.14); + + --font-sans: "Gen Interface JP", sans-serif; + --font-mono: "IBM Plex Mono", monospace; + --font-size-title: 24px; + --line-height-title: 32px; + --font-size-body: 14px; + --line-height-body: 20px; + --font-size-compact: 12px; + --line-height-compact: 16px; + + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 24px; + --space-6: 32px; + + --radius-soft: 8px; + --tui-green: #76946a; --tui-red: #c34043; --tui-yellow: #c0a36e; @@ -49,19 +77,8 @@ --tui-magenta: #957fb8; --tui-gray: #727169; --tui-dark-gray: #54546d; - --space-1: 4px; - --space-2: 8px; - --space-3: 12px; - --space-4: 16px; - --space-5: 24px; - --space-6: 32px; - --radius-soft: 8px; - --radius-panel: 12px; - --interactive-hover: oklch(94.5% 0 0); - --interactive-selected: oklch(93% 0.02 230); - --font-sans: "Gen Interface JP", sans-serif; - --font-mono: "IBM Plex Mono", monospace; } + @media (prefers-color-scheme: dark) { :root { --bg: oklch(16% 0 0); @@ -81,21 +98,23 @@ --danger: oklch(76% 0.14 25); --interactive-hover: oklch(29% 0 0); --interactive-selected: oklch(30% 0.025 230); + --shadow-overlay: 0 14px 36px oklch(0% 0 0 / 0.38); } } } + @layer base { html { - font-size: 14px; + font-size: var(--font-size-body); } + body { background: var(--bg); color: var(--text); font-family: var(--font-sans); + line-height: var(--line-height-body); } - a { - color: var(--accent); - } + h1, h2, h3, @@ -104,127 +123,56 @@ dd { overflow-wrap: anywhere; } + h1, h2, h3 { + margin-top: 0; color: var(--text-strong); - margin-top: 0; - } - p { - line-height: 1.55; - } - code, - pre { - font-family: var(--font-mono); - } - code { - color: var(--code); - } - small { - color: var(--text-muted); - display: block; - margin-top: var(--space-1); - } -} -@layer layout { - .grid { - display: grid; - gap: var(--space-5); - grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); - min-width: 0; - } - .runtime { - grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr)); - } - .stack { - display: grid; - gap: var(--space-4); - } -} -@layer components { - .section-note, - .section-state, - .muted { - color: var(--text-muted); - font-size: 0.82rem; - } - .section-note, - .section-state { - margin: 0; - line-height: 1.45; - } - .card { - min-width: 0; - padding: 0; - } - dl { - display: grid; - gap: var(--space-1); - margin: 0; - } - dl > div { - display: grid; - grid-template-columns: minmax(6.5rem, 10rem) minmax(0, 1fr); - align-items: baseline; - gap: var(--space-3); - min-width: 0; - } - dt { - color: var(--text-faint); - font-size: 0.72rem; - letter-spacing: 0.08em; - text-transform: uppercase; - white-space: nowrap; - } - dd { - min-width: 0; - margin: 0; - } - dd small { - display: inline; - margin-top: 0; - margin-left: var(--space-2); - } - .table-wrap { - overflow-x: auto; - } - table { - width: 100%; - border-collapse: collapse; - } - th, - td { - border-bottom: 1px solid var(--line); - padding: 10px 8px; - text-align: left; - vertical-align: top; - } - th { - color: var(--text-faint); - font-size: 0.78rem; - letter-spacing: 0.08em; - text-transform: uppercase; - } - .inline-link, - .secondary-button { - display: inline-flex; - align-items: center; - justify-content: center; - gap: var(--space-1); - border-radius: 999px; - border: 1px solid var(--line); - padding: 0.4rem 0.75rem; - background: var(--bg); - color: var(--accent); - font-size: 0.85rem; - font-weight: 800; - text-decoration: none; - cursor: pointer; } - .secondary-button:hover, - .inline-link:hover { - border-color: var(--line-strong); - background: var(--bg-raised); + h1 { + font-size: var(--font-size-title); + line-height: var(--line-height-title); + } + + h2, + h3 { + font-size: var(--font-size-body); + line-height: var(--line-height-body); + } + + p { + line-height: var(--line-height-body); + } + + code, + pre { + color: var(--code); + font-family: var(--font-mono); + } + + small { + color: var(--text-muted); + font-size: var(--font-size-compact); + line-height: var(--line-height-compact); + } + + :where( + a, + button, + input, + select, + textarea, + summary, + [tabindex] + ):focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + } + + ::selection { + background: var(--interactive-selected); + color: var(--text-strong); } } diff --git a/web/workspace/src/lib/workspace/alerts/WorkspaceAlerts.svelte b/web/workspace/src/lib/workspace/alerts/WorkspaceAlerts.svelte index 54bc0dfe..2dea47d8 100644 --- a/web/workspace/src/lib/workspace/alerts/WorkspaceAlerts.svelte +++ b/web/workspace/src/lib/workspace/alerts/WorkspaceAlerts.svelte @@ -96,7 +96,7 @@ display: block; margin-bottom: 0.15rem; color: var(--alert-color); - font-size: 0.82rem; + font-size: var(--font-size-body); letter-spacing: 0.02em; text-transform: uppercase; } @@ -105,8 +105,8 @@ margin: 0; overflow-wrap: anywhere; color: var(--text); - font-size: 0.9rem; - line-height: 1.35; + font-size: var(--font-size-body); + line-height: var(--line-height-body); } .workspace-alert-dismiss { @@ -118,7 +118,7 @@ background: transparent; color: var(--text-muted); cursor: pointer; - font-size: 1.2rem; + font-size: var(--font-size-body); line-height: 1; } diff --git a/web/workspace/src/lib/workspace/console/ComposerInput.svelte b/web/workspace/src/lib/workspace/console/ComposerInput.svelte index 9bb6235c..f905af84 100644 --- a/web/workspace/src/lib/workspace/console/ComposerInput.svelte +++ b/web/workspace/src/lib/workspace/console/ComposerInput.svelte @@ -586,7 +586,7 @@ border-radius: 999px; background: color-mix(in srgb, var(--accent) 10%, var(--bg-subtle)); color: var(--text-muted); - font-size: 0.72rem; + font-size: var(--font-size-compact); font-weight: 600; line-height: 1.35; text-overflow: ellipsis; diff --git a/web/workspace/src/lib/workspace/console/ConsoleLineItem.svelte b/web/workspace/src/lib/workspace/console/ConsoleLineItem.svelte index a53e3fa3..2686304e 100644 --- a/web/workspace/src/lib/workspace/console/ConsoleLineItem.svelte +++ b/web/workspace/src/lib/workspace/console/ConsoleLineItem.svelte @@ -192,7 +192,7 @@ .task-reminder-summary { margin: 0; color: var(--text-muted); - font-size: 0.78rem; + font-size: var(--font-size-compact); line-height: 1.55; white-space: pre-line; } @@ -203,7 +203,7 @@ gap: var(--space-2); color: var(--text-muted); font-family: var(--font-mono); - font-size: 0.78rem; + font-size: var(--font-size-compact); font-variant-numeric: tabular-nums; font-weight: 750; } @@ -213,7 +213,7 @@ padding-left: 1.25rem; color: var(--tui-dark-gray); font-family: var(--font-mono); - font-size: 0.75rem; + font-size: var(--font-size-compact); } .compaction-summary, @@ -227,15 +227,15 @@ display: block; color: var(--text-muted); font-family: var(--font-mono); - font-size: 0.7rem; + font-size: var(--font-size-compact); } .compaction-error { - color: var(--tui-error); + color: var(--danger); } .activity-summary { - font-size: 14px; + font-size: var(--font-size-body); } .task-reminder-summary { @@ -245,14 +245,14 @@ } .console-line.error .activity-summary { - color: var(--tui-error); + color: var(--danger); } .run-stats { margin: 0; color: var(--text-muted); font-family: var(--font-mono); - font-size: 0.72rem; + font-size: var(--font-size-compact); font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; @@ -294,7 +294,7 @@ min-width: 0; margin: 0; font-family: var(--font-mono); - font-size: 12px; + font-size: var(--font-size-compact); line-height: 1.1; overflow-x: auto; white-space: pre; @@ -313,7 +313,7 @@ align-items: baseline; gap: 0.5rem; color: var(--text-muted); - font-size: 0.88rem; + font-size: var(--font-size-compact); font-weight: 750; } @@ -329,7 +329,7 @@ .tool-status { flex: 0 0 auto; color: var(--tui-dark-gray); - font-size: 0.72rem; + font-size: var(--font-size-compact); white-space: nowrap; } @@ -357,7 +357,7 @@ justify-content: space-between; gap: var(--space-2); color: var(--text-muted); - font-size: 0.78rem; + font-size: var(--font-size-compact); font-weight: 750; } @@ -367,7 +367,7 @@ border-radius: 0.65rem; color: var(--text); font-family: var(--font-mono); - font-size: 0.78rem; + font-size: var(--font-size-compact); line-height: 1.45; margin: 0.6rem 0 0; overflow-x: auto; @@ -424,7 +424,7 @@ color: var(--text-muted); cursor: pointer; font: inherit; - font-size: 0.68rem; + font-size: var(--font-size-compact); font-weight: 750; opacity: 0; pointer-events: none; @@ -443,7 +443,7 @@ border-left: 2px solid var(--line); padding-left: 0.6rem; color: var(--text-muted); - font-size: 0.84rem; + font-size: var(--font-size-compact); } .message-detail p { diff --git a/web/workspace/src/lib/workspace/console/ConsoleTasks.svelte b/web/workspace/src/lib/workspace/console/ConsoleTasks.svelte index d4db74f1..c9c33843 100644 --- a/web/workspace/src/lib/workspace/console/ConsoleTasks.svelte +++ b/web/workspace/src/lib/workspace/console/ConsoleTasks.svelte @@ -124,7 +124,7 @@ gap: 0.1rem; min-width: 0; padding-inline: 0.75rem; - font-size: 0.8rem; + font-size: var(--font-size-compact); line-height: 1.35; } @@ -237,7 +237,7 @@ .task-pane h3 { margin: 0 0 1rem; color: var(--accent); - font-size: 0.9rem; + font-size: var(--font-size-body); } .task-empty { @@ -262,8 +262,8 @@ } .task-description { - font-size: 0.8rem; - line-height: 1.45; + font-size: var(--font-size-body); + line-height: var(--line-height-body); } @media (max-width: 900px) { diff --git a/web/workspace/src/lib/workspace/console/ConsoleTimeline.svelte b/web/workspace/src/lib/workspace/console/ConsoleTimeline.svelte index 6f590967..e67c01a5 100644 --- a/web/workspace/src/lib/workspace/console/ConsoleTimeline.svelte +++ b/web/workspace/src/lib/workspace/console/ConsoleTimeline.svelte @@ -158,8 +158,8 @@ text-align: left; border: 1px solid var(--line); border-radius: 10px; - background: color-mix(in srgb, var(--bg-panel) 92%, transparent); - box-shadow: var(--shadow-soft); + background: color-mix(in srgb, var(--bg-raised) 92%, transparent); + box-shadow: var(--shadow-overlay); opacity: 1; padding: 0.25rem 0.45rem; transform: none; diff --git a/web/workspace/src/lib/workspace/console/RichMarkdown.svelte b/web/workspace/src/lib/workspace/console/RichMarkdown.svelte index da585bd3..e9a56a71 100644 --- a/web/workspace/src/lib/workspace/console/RichMarkdown.svelte +++ b/web/workspace/src/lib/workspace/console/RichMarkdown.svelte @@ -69,7 +69,7 @@ :global(.rich-markdown h4) { margin: 0.7rem 0 0.35rem; color: var(--text-strong); - font-size: 1rem; + font-size: var(--font-size-body); } :global(.rich-markdown blockquote) { diff --git a/web/workspace/src/lib/workspace/console/WorkerRunStatus.svelte b/web/workspace/src/lib/workspace/console/WorkerRunStatus.svelte index b057d9f9..e43f025d 100644 --- a/web/workspace/src/lib/workspace/console/WorkerRunStatus.svelte +++ b/web/workspace/src/lib/workspace/console/WorkerRunStatus.svelte @@ -52,7 +52,7 @@ min-height: 1.35rem; color: var(--text-muted); font-family: var(--font-mono); - font-size: 0.74rem; + font-size: var(--font-size-compact); font-variant-numeric: tabular-nums; } diff --git a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts index 3b6629ae..8b1d29e2 100644 --- a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts +++ b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts @@ -78,6 +78,134 @@ Deno.test("workspace feature css is owned outside app css", async () => { ); }); +Deno.test("workspace app css is limited to global foundation", async () => { + const appCss = await Deno.readTextFile( + new URL("./../../../app.css", import.meta.url), + ); + const workspacePagesCss = await Deno.readTextFile( + new URL( + "./../styles/workspace-pages.css", + import.meta.url, + ), + ); + const settingsCss = await Deno.readTextFile( + new URL("./../styles/settings.css", import.meta.url), + ); + const sidebarCss = await Deno.readTextFile( + new URL("./../sidebar/sidebar.css", import.meta.url), + ); + + for ( + const selector of [ + ".card", + ".stack", + ".section-note", + ".section-state", + ".muted", + ".table-wrap", + ".inline-link", + ".secondary-button", + ] + ) { + assert( + !appCss.includes(selector), + `app.css must not own feature selector ${selector}`, + ); + } + + assert( + !/^\s*a\s*\{[^}]*\bcolor\s*:/ms.test(appCss), + "app.css must not override component-owned link colors", + ); + + for ( + const legacyToken of [ + "--surface", + "--border", + "--bg-panel", + "--interactive-muted", + "--radius-card", + "--radius-panel", + "--shadow-soft", + "--tui-error", + ] + ) { + assert( + !appCss.includes(legacyToken), + `app.css must not retain legacy token ${legacyToken}`, + ); + } + + assert( + workspacePagesCss.includes(".main-content .card") && + workspacePagesCss.includes(".main-content table") && + workspacePagesCss.includes(".main-content dl"), + "workspace content primitives must be scoped to the main content owner", + ); + assert( + settingsCss.includes(".main-content .inline-link") && + settingsCss.includes(".main-content .secondary-button"), + "settings CSS must own its link and button variants", + ); + assert( + sidebarCss.includes(".section-state"), + "sidebar CSS must own sidebar status presentation", + ); +}); + +Deno.test("design lab demonstrates nested sidebar slots without flattening levels", async () => { + const workspaceLayout = await Deno.readTextFile( + new URL( + "./../../../routes/design-lab/workspace-web-ux/+layout.svelte", + import.meta.url, + ), + ); + const settingsLayout = await Deno.readTextFile( + new URL( + "./../../../routes/design-lab/workspace-web-ux/settings/+layout.svelte", + import.meta.url, + ), + ); + + assert( + workspaceLayout.includes("createOverrideStack"), + "Workspace design-lab layout must provide a child sidebar slot", + ); + assert( + workspaceLayout.includes(""), + "Settings design-lab layout must provide a child sidebar slot", + ); + assert( + settingsLayout.includes(" { const workspacePage = await Deno.readTextFile( new URL("./../../../routes/w/[workspaceId]/+page.svelte", import.meta.url), @@ -420,7 +548,7 @@ Deno.test("Worker Console overview activity summaries use 14px text", async () = assert( consoleLine.includes(".activity-summary {") && - consoleLine.includes("font-size: 14px;"), + consoleLine.includes("font-size: var(--font-size-body);"), "Overview activity summaries such as ran command counts should render at 14px", ); }); @@ -443,7 +571,7 @@ Deno.test("Worker Console renders markdown only for message rows", async () => { consoleLine.includes( ".console-line.tool.tool-bash .console-plain-text", ) && - consoleLine.includes("font-size: 12px;") && + consoleLine.includes("font-size: var(--font-size-compact);") && consoleLine.includes("line-height: 1.1;") && consoleLine.includes("{:else if shouldRenderMarkdown(item)}") && consoleLine.includes("") && @@ -920,10 +1048,12 @@ Deno.test("Account UI owns browser passkey session state without workspace autho "Workspace layout should load workspace data, register with the parent slot, and provide the same slot contract to children", ); assert( - sidebarFrame.includes("let folded = $state(false)") && + sidebarFrame.includes("folded = $bindable(false)") && sidebarFrame.includes("sidebar-fold-button") && sidebarFrame.includes("Fold sidebar") && sidebarFrame.includes("Unfold sidebar") && + rootLayout.includes("let sidebarFolded = $state(false)") && + rootLayout.includes("") && !workspaceLayout.includes("sidebarFolded") && !workspaceLayout.includes("onToggleFold") && !sidebar.includes("folded?: boolean") && diff --git a/web/workspace/src/lib/workspace/header/WorkspaceBreadcrumbs.svelte b/web/workspace/src/lib/workspace/header/WorkspaceBreadcrumbs.svelte index 0f8c2f85..b41c1787 100644 --- a/web/workspace/src/lib/workspace/header/WorkspaceBreadcrumbs.svelte +++ b/web/workspace/src/lib/workspace/header/WorkspaceBreadcrumbs.svelte @@ -61,7 +61,7 @@ gap: 0.55rem; color: var(--text-muted); font-family: var(--font-mono); - font-size: 0.84rem; + font-size: var(--font-size-compact); line-height: 1; } diff --git a/web/workspace/src/lib/workspace/settings/DecodalSourceEditor.svelte b/web/workspace/src/lib/workspace/settings/DecodalSourceEditor.svelte index 6835d02a..175d8d20 100644 --- a/web/workspace/src/lib/workspace/settings/DecodalSourceEditor.svelte +++ b/web/workspace/src/lib/workspace/settings/DecodalSourceEditor.svelte @@ -65,7 +65,7 @@ '.cm-content': { padding: '0.75rem 0', caretColor: 'var(--text-strong)' }, '.cm-fixed-schema-wrapper': { color: 'var(--text-muted)', - backgroundColor: 'var(--interactive-muted)', + backgroundColor: 'var(--interactive-hover)', fontWeight: '600', }, '.cm-cursor, .cm-dropCursor': { borderLeftColor: 'var(--text-strong)', borderLeftWidth: '2px' }, diff --git a/web/workspace/src/lib/workspace/sidebar/SidebarFrame.svelte b/web/workspace/src/lib/workspace/sidebar/SidebarFrame.svelte index 5097eed5..eba27285 100644 --- a/web/workspace/src/lib/workspace/sidebar/SidebarFrame.svelte +++ b/web/workspace/src/lib/workspace/sidebar/SidebarFrame.svelte @@ -1,46 +1,51 @@ - + + + + + diff --git a/web/workspace/src/lib/workspace/sidebar/sidebar.css b/web/workspace/src/lib/workspace/sidebar/sidebar.css index b6394d16..dfb0e5c1 100644 --- a/web/workspace/src/lib/workspace/sidebar/sidebar.css +++ b/web/workspace/src/lib/workspace/sidebar/sidebar.css @@ -5,16 +5,27 @@ --sidebar-item-hover: oklch(96% 0 0); --sidebar-item-active: oklch(92.5% 0 0); + display: grid; grid-column: 1; grid-row: 1 / 3; - display: flex; - flex-direction: column; width: clamp(220px, 20vw, 280px); min-width: 0; min-height: 0; overflow: hidden; + } + .sidebar-frame .bevel.sidebar-frame__bevel { + width: 100%; + min-width: 0; + min-height: 0; + } + .sidebar-frame__surface { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + min-width: 0; + min-height: 0; padding-block: var(--space-4); - border-right: 1px solid var(--line); } @media (prefers-color-scheme: dark) { .sidebar-frame { @@ -25,7 +36,6 @@ .sidebar-frame.folded { width: max-content; overflow: hidden; - padding-inline: 0; } .sidebar-frame-content { flex: 1 1 auto; @@ -112,7 +122,7 @@ border-radius: var(--radius-soft); padding: 0.45rem 0.6rem; color: var(--text-strong); - font-size: 1.05rem; + font-size: var(--font-size-compact); font-weight: 800; line-height: 1.25; text-overflow: ellipsis; @@ -139,7 +149,7 @@ background: transparent; color: var(--text-strong); font: inherit; - font-size: 1.05rem; + font-size: var(--font-size-compact); font-weight: 800; line-height: 1.25; text-align: left; @@ -174,7 +184,7 @@ padding: 0.35rem 0.45rem; color: var(--text-muted); font-family: var(--font-mono); - font-size: 0.84rem; + font-size: var(--font-size-compact); font-weight: 650; line-height: 1; } @@ -207,7 +217,7 @@ border-radius: calc(var(--radius-soft) + 0.2rem); padding: var(--space-2); background: var(--bg-raised); - box-shadow: var(--shadow-soft); + box-shadow: var(--shadow-overlay); } .workspace-menu-item { display: flex; @@ -217,7 +227,7 @@ border-radius: var(--radius-soft); padding: var(--space-2); color: var(--text-strong); - font-size: 0.82rem; + font-size: var(--font-size-compact); font-weight: 650; line-height: 1.3; text-decoration: none; @@ -247,7 +257,7 @@ gap: var(--space-2); padding: var(--space-1) var(--space-2) 0; color: var(--text-faint); - font-size: 0.68rem; + font-size: var(--font-size-compact); font-weight: 750; letter-spacing: 0.08em; text-transform: uppercase; @@ -259,7 +269,7 @@ place-items: center; border-radius: var(--radius-soft); color: var(--text-muted); - font-size: 1rem; + font-size: var(--font-size-body); line-height: 1; text-decoration: none; } @@ -281,16 +291,22 @@ .workspace-menu-status { margin: var(--space-1) var(--space-2); color: var(--text-muted); - font-size: 0.72rem; + font-size: var(--font-size-compact); line-height: 1.35; } .workspace-menu-status.error { color: var(--danger); } + .section-state { + margin: var(--space-2) 0 0; + color: var(--text-muted); + font-size: var(--font-size-compact); + line-height: 1.35; + } .workspace-status { margin: var(--space-1) 0 0; color: var(--text-muted); - font-size: 0.78rem; + font-size: var(--font-size-compact); line-height: 1.35; } .workspace-status.error, @@ -311,7 +327,7 @@ .sidebar-fold-button { border: 1px solid var(--line); background: var(--bg-raised); - box-shadow: var(--shadow-soft); + box-shadow: var(--shadow-overlay); color: var(--text-strong); cursor: pointer; } @@ -349,7 +365,7 @@ .sidebar-nav-section__header, .sidebar-section-label { color: var(--text-faint); - font-size: 0.72rem; + font-size: var(--font-size-compact); font-weight: 750; letter-spacing: 0.11em; text-transform: uppercase; @@ -373,7 +389,7 @@ } .section-count { color: var(--text-muted); - font-size: 0.72rem; + font-size: var(--font-size-compact); line-height: 1; } .nav-list, @@ -392,7 +408,7 @@ padding: var(--space-2) var(--space-3); border-radius: var(--radius-soft); color: var(--text-strong); - font-size: 0.9rem; + font-size: var(--font-size-compact); text-align: left; text-decoration: none; transition: background-color 140ms ease, color 140ms ease; @@ -465,7 +481,7 @@ border-radius: var(--radius-soft); padding: var(--space-1); background: var(--bg-raised); - box-shadow: var(--shadow-soft); + box-shadow: var(--shadow-overlay); } .worker-actions-menu button { border: 0; @@ -474,7 +490,7 @@ background: transparent; color: var(--text-strong); font: inherit; - font-size: 0.76rem; + font-size: var(--font-size-compact); font-weight: 650; line-height: 1.2; text-align: left; @@ -535,7 +551,7 @@ align-items: center; justify-content: center; width: 0.75rem; - font-size: 0.7rem; + font-size: var(--font-size-compact); line-height: 1; } .worker-status-spinner.is-subworker { @@ -547,7 +563,7 @@ min-width: 0; overflow: hidden; color: inherit; - font-size: 0.78rem; + font-size: var(--font-size-compact); font-weight: 600; line-height: 1.1rem; text-overflow: ellipsis; @@ -560,7 +576,7 @@ overflow: hidden; color: var(--text-muted); font-family: var(--font-mono); - font-size: 0.66rem; + font-size: var(--font-size-compact); line-height: 1rem; text-overflow: ellipsis; white-space: nowrap; @@ -608,13 +624,18 @@ } @media (max-width: 760px) { - .sidebar-frame, - .sidebar-frame.folded { + .sidebar-frame { + z-index: 20; grid-column: 1; - grid-row: 1; - width: auto; - border-right: 0; - border-bottom: 1px solid var(--line); + grid-row: 2; + width: 100%; + height: 100%; + } + .sidebar-frame.folded { + display: none; + } + .sidebar-control-row { + display: none; } } } diff --git a/web/workspace/src/lib/workspace/styles/settings.css b/web/workspace/src/lib/workspace/styles/settings.css index 1a2f6593..d81fbb49 100644 --- a/web/workspace/src/lib/workspace/styles/settings.css +++ b/web/workspace/src/lib/workspace/styles/settings.css @@ -1,5 +1,40 @@ @layer reset, tokens, base, layout, components; @layer components { + .main-content .muted { + color: var(--text-muted); + } + + .main-content .inline-link { + color: var(--accent); + font-weight: 700; + text-underline-offset: 0.15em; + } + + .main-content .secondary-button { + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid var(--line); + border-radius: var(--radius-soft); + padding: 0.45rem 0.75rem; + background: var(--bg-raised); + color: var(--text-strong); + font-weight: 700; + text-decoration: none; + cursor: pointer; + } + + .main-content .secondary-button:hover:not(:disabled), + .main-content .secondary-button:focus-visible:not(:disabled) { + border-color: var(--accent); + background: var(--interactive-hover); + } + + .main-content .secondary-button:disabled { + cursor: not-allowed; + opacity: 0.55; + } + .settings-button { text-decoration: none; cursor: pointer; @@ -32,17 +67,25 @@ } .badge { + display: inline-flex; flex: 0 0 auto; - border-radius: 999px; - padding: 0.35rem 0.65rem; - background: var(--bg-subtle); + align-items: center; + gap: var(--space-1); color: var(--text-muted); - font-size: 0.72rem; + font-size: var(--font-size-compact); font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; } + .badge::before { + width: 0.45rem; + height: 0.45rem; + border-radius: 50%; + background: currentColor; + content: ""; + } + .badge.warning { color: var(--warning); } @@ -62,7 +105,6 @@ max-width: 24rem; padding: var(--space-3) var(--space-4); border: 1px solid var(--line); - border-radius: var(--radius-panel); background: var(--bg-raised); color: var(--text-muted); } @@ -83,7 +125,6 @@ min-width: min(16rem, 100%); padding: var(--space-3) var(--space-4); border: 1px solid var(--line); - border-radius: var(--radius-panel); background: var(--bg-raised); color: inherit; text-decoration: none; @@ -128,7 +169,6 @@ .settings-pattern { padding: var(--space-4); - border-radius: var(--radius-panel); background: var(--bg-raised); } @@ -162,7 +202,7 @@ .account-details dt { color: var(--text-muted); - font-size: 0.8rem; + font-size: var(--font-size-compact); font-weight: 700; text-transform: uppercase; } @@ -173,7 +213,7 @@ } .account-details code { - font-size: 0.78rem; + font-size: var(--font-size-compact); } .account-panel { @@ -192,7 +232,7 @@ display: grid; gap: 0.25rem; color: var(--text-muted); - font-size: 0.78rem; + font-size: var(--font-size-body); } .settings-runtime-form button, @@ -223,7 +263,7 @@ border: 1px solid var(--line); border-radius: 1rem; padding: 1rem; - background: rgba(255, 255, 255, 0.03); + background: color-mix(in srgb, var(--text-strong) 3%, transparent); } .settings-runtime-table-wrap { @@ -249,7 +289,7 @@ .settings-runtime-table th { color: var(--text-muted); - font-size: 0.72rem; + font-size: var(--font-size-compact); letter-spacing: 0.05em; text-transform: uppercase; } @@ -288,12 +328,12 @@ .settings-runtime-detail-row td { padding-top: 0; - background: rgba(255, 255, 255, 0.025); + background: color-mix(in srgb, var(--text-strong) 2.5%, transparent); } .settings-muted-action { color: var(--text-muted); - font-size: 0.82rem; + font-size: var(--font-size-compact); } .settings-action-row { @@ -325,18 +365,18 @@ } .settings-diagnostics-list li.error { - border-color: rgba(255, 99, 99, 0.55); + border-color: color-mix(in srgb, var(--danger) 55%, transparent); } .settings-diagnostics-list li.warning { - border-color: rgba(255, 205, 86, 0.55); + border-color: color-mix(in srgb, var(--warning) 55%, transparent); } .settings-test-result { display: grid; gap: 0.3rem; border-radius: 0.75rem; - background: rgba(255, 255, 255, 0.04); + background: color-mix(in srgb, var(--text-strong) 4%, transparent); padding: 0.75rem; } .settings-test-result.failed { @@ -374,7 +414,7 @@ .runtime-detail-grid dt { margin-bottom: var(--space-1); color: var(--text-muted); - font-size: 0.72rem; + font-size: var(--font-size-compact); font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; @@ -433,7 +473,7 @@ background: var(--bg-raised); color: var(--text-strong); font-family: var(--font-mono); - font-size: 0.78rem; + font-size: var(--font-size-compact); } .runtime-public-key { @@ -468,7 +508,7 @@ .runtime-trust-form label, .runtime-revoke-row label { color: var(--text-muted); - font-size: 0.78rem; + font-size: var(--font-size-compact); font-weight: 700; } @@ -503,7 +543,7 @@ .runtime-trust-comparison dt { color: var(--text-muted); - font-size: 0.72rem; + font-size: var(--font-size-compact); font-weight: 700; } @@ -559,7 +599,7 @@ .runtime-audit-table th { color: var(--text-muted); - font-size: 0.72rem; + font-size: var(--font-size-compact); letter-spacing: 0.05em; text-transform: uppercase; } @@ -589,7 +629,6 @@ min-width: min(16rem, 100%); padding: var(--space-3) var(--space-4); border: 1px solid var(--line); - border-radius: var(--radius-panel); background: var(--bg-raised); } .settings-section-card:hover, @@ -628,7 +667,7 @@ display: grid; gap: var(--space-1); color: var(--text-muted); - font-size: 0.82rem; + font-size: var(--font-size-body); } .settings-form input, .settings-form select, @@ -685,7 +724,6 @@ gap: var(--space-2); padding: var(--space-3); border: 1px solid var(--line); - border-radius: var(--radius-panel); background: var(--bg-raised); } .settings-profile-list strong, @@ -721,29 +759,35 @@ } .settings-identity-list dt { color: var(--text-muted); - font-size: 0.78rem; + font-size: var(--font-size-compact); } .settings-identity-list dd { margin: 0; color: var(--text-strong); } .section-status-pill { + display: inline-flex; width: fit-content; - border-radius: 999px; - padding: 0.25rem 0.55rem; - background: var(--bg-subtle); + align-items: center; + gap: var(--space-1); color: var(--text-muted); - font-size: 0.72rem; + font-size: var(--font-size-compact); font-weight: 800; text-transform: uppercase; } + .section-status-pill::before { + width: 0.45rem; + height: 0.45rem; + border-radius: 50%; + background: currentColor; + content: ""; + } .config-source-shell { display: grid; grid-template-columns: minmax(12rem, 18rem) minmax(0, 1fr); min-height: 40rem; overflow: hidden; border: 1px solid var(--line); - border-radius: var(--radius-panel); background: var(--bg-raised); } .config-source-tree { @@ -784,7 +828,7 @@ } .config-source-tree nav button small { color: var(--text-muted); - font-size: 0.65rem; + font-size: var(--font-size-compact); letter-spacing: 0.06em; text-transform: uppercase; } @@ -799,10 +843,13 @@ padding: var(--space-3); border-top: 1px solid var(--line); } - .config-source-create label, + .config-source-create label { + color: var(--text-muted); + font-size: var(--font-size-body); + } .config-source-workbench__header span { color: var(--text-muted); - font-size: 0.75rem; + font-size: var(--font-size-compact); } .config-source-create input, .config-source-actions input { @@ -861,7 +908,7 @@ padding: var(--space-2) var(--space-3); border-top: 1px solid var(--line); color: var(--text-muted); - font-size: 0.8rem; + font-size: var(--font-size-compact); } .config-source-conflict { display: flex; @@ -924,7 +971,6 @@ gap: var(--space-4); padding: var(--space-5); border: 1px solid var(--line); - border-radius: var(--radius-panel); background: var(--bg-raised); } .settings-repository-form h2 { @@ -934,7 +980,7 @@ display: grid; gap: var(--space-2); color: var(--text-muted); - font-size: 0.78rem; + font-size: var(--font-size-body); font-weight: 700; } .settings-repository-form input { @@ -963,7 +1009,6 @@ .settings-repository-table-wrap { overflow-x: auto; border: 1px solid var(--line); - border-radius: var(--radius-panel); background: var(--bg-raised); } .settings-repository-table { @@ -979,7 +1024,7 @@ } .settings-repository-table th { color: var(--text-muted); - font-size: 0.72rem; + font-size: var(--font-size-compact); text-transform: uppercase; } .settings-repository-table tbody tr:last-child td { diff --git a/web/workspace/src/lib/workspace/styles/tickets.css b/web/workspace/src/lib/workspace/styles/tickets.css index a55ec02a..8790b87b 100644 --- a/web/workspace/src/lib/workspace/styles/tickets.css +++ b/web/workspace/src/lib/workspace/styles/tickets.css @@ -10,7 +10,7 @@ border-radius: 0.55rem; padding: 0.5rem 0.8rem; font: inherit; - font-size: 0.82rem; + font-size: var(--font-size-compact); font-weight: 700; text-decoration: none; cursor: pointer; @@ -26,9 +26,9 @@ color: var(--text-strong); } .workspace-danger-button { - border: 1px solid #a63f3f; - background: #a63f3f; - color: #fff; + border: 1px solid var(--danger); + background: var(--danger); + color: var(--bg-raised); } .workspace-primary-button:disabled, .workspace-secondary-button:disabled, @@ -38,7 +38,7 @@ } .workspace-back-link { color: var(--text-muted); - font-size: 0.82rem; + font-size: var(--font-size-compact); text-decoration: none; } .workspace-callout { @@ -47,8 +47,8 @@ padding: 0.7rem 0.85rem; } .workspace-callout.is-error { - border-color: #a63f3f; - color: #d66; + border-color: var(--danger); + color: var(--danger); } .workspace-empty-copy { margin: 0; @@ -94,20 +94,20 @@ } .orchestrator-status strong { color: var(--text-strong); - font-size: 0.76rem; + font-size: var(--font-size-compact); } .orchestrator-status span { color: var(--text-muted); - font-size: 0.68rem; + font-size: var(--font-size-compact); } .orchestrator-status-dot { width: 0.55rem; height: 0.55rem; border-radius: 50%; - background: #a75454; + background: var(--danger); } .orchestrator-status[data-online="true"] .orchestrator-status-dot { - background: #43a66d; + background: var(--success); } .ticket-panel-summary { display: grid; @@ -116,8 +116,8 @@ } .ticket-panel-summary strong { color: var(--text-strong); - font-size: 1.8rem; - line-height: 1; + font-size: var(--font-size-compact); + line-height: var(--line-height-compact); } .ticket-kanban { display: grid; @@ -149,7 +149,7 @@ } .ticket-lane-header h2 { margin: 0; - font-size: 0.78rem; + font-size: var(--font-size-compact); text-transform: uppercase; letter-spacing: 0.04em; } @@ -160,17 +160,17 @@ background: var(--text-muted); } .ticket-lane[data-state="ready-planning"] .ticket-state-dot { - background: #43a66d; + background: var(--success); } .ticket-lane[data-state="inprogress-queued"] .ticket-state-dot { - background: #d09b37; + background: var(--warning); } .ticket-lane[data-state="done-closed"] .ticket-state-dot { background: var(--accent); } .ticket-lane-count { color: var(--text-muted); - font-size: 0.75rem; + font-size: var(--font-size-compact); } .ticket-lane-cards { display: grid; @@ -188,7 +188,7 @@ margin: 0; color: var(--text-muted); padding: 0.5rem; - font-size: 0.7rem; + font-size: var(--font-size-compact); text-align: center; } .ticket-lane-page-state { @@ -198,7 +198,7 @@ margin: 0; padding: 0.45rem; color: var(--text-muted); - font-size: 0.72rem; + font-size: var(--font-size-compact); text-align: center; } .ticket-lane-page-error { @@ -229,17 +229,17 @@ overflow: hidden; color: var(--text-muted); font-family: var(--font-mono); - font-size: 0.68rem; + font-size: var(--font-size-compact); text-overflow: ellipsis; } .ticket-card strong { - font-size: 0.85rem; + font-size: var(--font-size-compact); line-height: 1.35; } .ticket-card-meta { justify-content: space-between; color: var(--text-muted); - font-size: 0.68rem; + font-size: var(--font-size-compact); } .ticket-risk-list { display: flex; @@ -247,16 +247,23 @@ gap: 0.25rem; } .ticket-risk-list span { - border: 1px solid #a97531; - border-radius: 999px; - color: #d3a55d; - padding: 0.1rem 0.35rem; - font-size: 0.62rem; + display: inline-flex; + align-items: center; + gap: var(--space-1); + color: var(--warning); + font-size: var(--font-size-compact); + } + .ticket-risk-list span::before { + width: 0.4rem; + height: 0.4rem; + border-radius: 50%; + background: currentColor; + content: ""; } .ticket-lane-empty { padding: 1.2rem 0.4rem; color: var(--text-muted); - font-size: 0.75rem; + font-size: var(--font-size-compact); text-align: center; } .ticket-detail-page { @@ -267,28 +274,35 @@ .ticket-detail-header h1 { margin: 0.45rem 0 0.25rem; color: var(--text-strong); - font-size: clamp(1.5rem, 3vw, 2.4rem); + font-size: var(--font-size-title); } .ticket-detail-header p { margin: 0; color: var(--text-muted); - font-size: 0.78rem; + font-size: var(--font-size-compact); } .ticket-detail-kicker { gap: 0.6rem; } .ticket-detail-kicker code { color: var(--text-muted); - font-size: 0.72rem; + font-size: var(--font-size-compact); } .workspace-status-pill { - border: 1px solid var(--line); - border-radius: 999px; - padding: 0.15rem 0.45rem; + display: inline-flex; + align-items: center; + gap: var(--space-1); color: var(--text-muted); - font-size: 0.68rem; + font-size: var(--font-size-compact); text-transform: uppercase; } + .workspace-status-pill::before { + width: 0.45rem; + height: 0.45rem; + border-radius: 50%; + background: currentColor; + content: ""; + } .ticket-detail-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(18rem, 24rem); @@ -318,11 +332,11 @@ .ticket-section-heading h2, .ticket-control-card h2 { margin: 0; color: var(--text-strong); - font-size: 0.9rem; + font-size: var(--font-size-body); } .ticket-section-heading span, .ticket-control-card header span { color: var(--text-muted); - font-size: 0.7rem; + font-size: var(--font-size-compact); } .ticket-editor, .ticket-control-form { display: grid; @@ -332,7 +346,7 @@ display: grid; gap: 0.3rem; color: var(--text-muted); - font-size: 0.72rem; + font-size: var(--font-size-body); } .ticket-editor input, .ticket-editor textarea, @@ -349,8 +363,8 @@ } .ticket-worker-card p { color: var(--text-muted); - font-size: 0.78rem; - line-height: 1.5; + font-size: var(--font-size-body); + line-height: var(--line-height-body); } .ticket-role-actions { gap: 0.55rem; @@ -365,14 +379,14 @@ details.ticket-control-card summary { cursor: pointer; color: var(--text-strong); - font-size: 0.85rem; + font-size: var(--font-size-compact); font-weight: 700; } details.ticket-control-card[open] summary { margin-bottom: var(--space-3); } .ticket-close-card { - border-color: color-mix(in srgb, #a63f3f 55%, var(--line)); + border-color: color-mix(in srgb, var(--danger) 55%, var(--line)); } .ticket-relations-list, .ticket-blocker-list { display: grid; @@ -391,13 +405,13 @@ text-decoration: none; } .ticket-blocker-list a { - border-color: #a97531; + border-color: var(--warning); } .ticket-relations-list span, .ticket-relations-list small, .ticket-blocker-list span { color: var(--text-muted); - font-size: 0.68rem; + font-size: var(--font-size-compact); } .ticket-timeline { display: grid; @@ -435,7 +449,7 @@ } .ticket-timeline time, .ticket-event-author { color: var(--text-muted); - font-size: 0.68rem; + font-size: var(--font-size-compact); } .ticket-event-author { margin: 0.2rem 0; diff --git a/web/workspace/src/lib/workspace/styles/workers.css b/web/workspace/src/lib/workspace/styles/workers.css index 081330d2..b9146d2d 100644 --- a/web/workspace/src/lib/workspace/styles/workers.css +++ b/web/workspace/src/lib/workspace/styles/workers.css @@ -9,10 +9,10 @@ .section-action { margin-left: auto; border: 1px solid var(--line); - border-radius: 999px; + border-radius: var(--radius-soft); background: var(--bg-subtle); color: var(--text-strong); - font-size: 0.72rem; + font-size: var(--font-size-compact); padding: 0.2rem 0.55rem; cursor: pointer; text-decoration: none; @@ -35,7 +35,7 @@ display: grid; gap: 0.25rem; color: var(--text-muted); - font-size: 0.78rem; + font-size: var(--font-size-body); } .worker-launch-form input, @@ -85,8 +85,8 @@ } .worker-new-page-header h1 { color: var(--text-strong); - font-size: clamp(1.6rem, 4vw, 2.4rem); - line-height: 1.05; + font-size: var(--font-size-title); + line-height: var(--line-height-title); } .worker-new-page-header p:not(.eyebrow) { margin-top: var(--space-2); @@ -110,11 +110,11 @@ .worker-ticket-context span, .worker-ticket-context code { color: var(--text-muted); - font-size: 0.72rem; + font-size: var(--font-size-compact); } .worker-ticket-context a { color: var(--accent); - font-size: 0.78rem; + font-size: var(--font-size-compact); } .worker-launch-form { display: grid; @@ -128,14 +128,14 @@ .worker-form-section h2 { margin: 0; color: var(--text-strong); - font-size: 1rem; + font-size: var(--font-size-body); } .worker-launch-sentence { display: flex; align-items: center; gap: var(--space-2); color: var(--text-muted); - font-size: 0.95rem; + font-size: var(--font-size-body); white-space: nowrap; } .worker-inline-select { @@ -154,7 +154,7 @@ .new-working-directory-panel h3 { margin: 0; color: var(--text-strong); - font-size: 0.95rem; + font-size: var(--font-size-body); } .new-working-directory-fields, .worker-detail-grid { diff --git a/web/workspace/src/lib/workspace/styles/workspace-catalog.css b/web/workspace/src/lib/workspace/styles/workspace-catalog.css index 6dee8d2d..487eb10c 100644 --- a/web/workspace/src/lib/workspace/styles/workspace-catalog.css +++ b/web/workspace/src/lib/workspace/styles/workspace-catalog.css @@ -1,3 +1,4 @@ +@layer reset, tokens, base, layout, components; @layer components { .workspace-catalog-shell { width: min(1120px, calc(100% - 2rem)); @@ -32,7 +33,7 @@ .workspace-catalog-eyebrow { color: var(--accent) !important; - font-size: 0.75rem; + font-size: var(--font-size-compact); font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; @@ -74,13 +75,22 @@ } .workspace-card-heading > span { - border: 1px solid var(--line); - border-radius: 999px; - padding: 0.15rem 0.5rem; - font-size: 0.7rem; + display: inline-flex; + align-items: center; + gap: var(--space-1); + color: var(--text-muted); + font-size: var(--font-size-compact); text-transform: uppercase; } + .workspace-card-heading > span::before { + width: 0.45rem; + height: 0.45rem; + border-radius: 50%; + background: currentColor; + content: ""; + } + .workspace-card-heading > .workspace-state-active { border-color: color-mix(in srgb, var(--success) 50%, var(--line)); color: var(--success); @@ -112,7 +122,7 @@ display: grid; gap: 0.35rem; color: var(--text-muted); - font-size: 0.8rem; + font-size: var(--font-size-body); } .workspace-create-panel input, @@ -172,7 +182,7 @@ } .workspace-switcher label { - font-size: 0.68rem; + font-size: var(--font-size-compact); letter-spacing: 0.08em; color: var(--text-muted); text-transform: uppercase; @@ -181,18 +191,18 @@ .workspace-switcher select { width: 100%; padding: 0.45rem 0.55rem; - font-size: 0.78rem; + font-size: var(--font-size-compact); } .workspace-switcher-actions { display: flex; gap: 0.75rem; - font-size: 0.72rem; + font-size: var(--font-size-compact); } .workspace-switcher-error { color: var(--danger); - font-size: 0.7rem; + font-size: var(--font-size-compact); } @media (max-width: 760px) { diff --git a/web/workspace/src/lib/workspace/styles/workspace-pages.css b/web/workspace/src/lib/workspace/styles/workspace-pages.css index c599c796..c244f6ff 100644 --- a/web/workspace/src/lib/workspace/styles/workspace-pages.css +++ b/web/workspace/src/lib/workspace/styles/workspace-pages.css @@ -1,5 +1,63 @@ @layer reset, tokens, base, layout, components; @layer components { + .main-content .stack { + display: grid; + gap: var(--space-2); + } + + .main-content .card { + min-width: 0; + padding: 0; + } + + .main-content .section-note { + margin: 0; + color: var(--text-muted); + } + + .main-content .section-state { + margin: var(--space-3) 0 0; + color: var(--text-muted); + } + + .main-content .section-state.error { + color: var(--danger); + } + + .main-content .muted { + color: var(--text-muted); + } + + .main-content .secondary-button { + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid var(--line); + border-radius: var(--radius-soft); + padding: 0.45rem 0.75rem; + background: var(--bg-raised); + color: var(--text-strong); + font-weight: 700; + text-decoration: none; + cursor: pointer; + } + + .main-content .secondary-button:hover:not(:disabled), + .main-content .secondary-button:focus-visible:not(:disabled) { + border-color: var(--accent); + background: var(--interactive-hover); + } + + .main-content .secondary-button:disabled { + cursor: not-allowed; + opacity: 0.55; + } + + .main-content small { + display: block; + margin-top: var(--space-1); + } + .workspace-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -10,7 +68,6 @@ gap: var(--space-1); padding: var(--space-4); border: 1px solid var(--line); - border-radius: var(--radius-card); background: var(--bg-raised); color: inherit; text-decoration: none; @@ -24,7 +81,7 @@ color: var(--text-muted); } .workspace-action-card span { - font-size: 0.78rem; + font-size: var(--font-size-compact); } .workspace-action-card strong { color: var(--text-strong); @@ -51,7 +108,6 @@ align-items: start; gap: var(--space-4); padding: var(--space-3) var(--space-4); - border-radius: var(--radius-panel); background: var(--bg-raised); color: inherit; text-decoration: none; @@ -96,7 +152,7 @@ .state-pill { flex: 0 0 auto; color: var(--success); - font-size: 0.76rem; + font-size: var(--font-size-compact); font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase; @@ -105,7 +161,7 @@ .objective-summary { margin: 0; color: var(--text-muted); - font-size: 0.88rem; + font-size: var(--font-size-compact); line-height: 1.4; } .ticket-meta, @@ -115,7 +171,7 @@ justify-content: flex-end; gap: var(--space-2); color: var(--text-faint); - font-size: 0.78rem; + font-size: var(--font-size-compact); line-height: 1.35; text-align: right; white-space: nowrap; @@ -155,7 +211,7 @@ } .detail-heading span { color: var(--text-muted); - font-size: 0.8rem; + font-size: var(--font-size-compact); } .ticket-detail-grid { margin-bottom: var(--space-4); @@ -168,70 +224,77 @@ } .risk-flags span, .warning-pill { - border-radius: 999px; - padding: 0.18rem 0.55rem; - background: var(--bg-subtle); + display: inline-flex; + align-items: center; + gap: var(--space-1); color: var(--warning); - font-size: 0.72rem; + font-size: var(--font-size-compact); font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; } + .risk-flags span::before, + .warning-pill::before { + width: 0.45rem; + height: 0.45rem; + border-radius: 50%; + background: currentColor; + content: ""; + } .ticket-body pre { overflow-x: auto; margin: 0; padding: var(--space-4); border: 1px solid var(--line); - border-radius: var(--radius-panel); background: var(--bg-raised); color: var(--text); white-space: pre-wrap; } - dl { + .main-content dl { display: grid; gap: var(--space-1); margin: 0; } - dl > div { + .main-content dl > div { display: grid; grid-template-columns: minmax(6.5rem, 10rem) minmax(0, 1fr); align-items: baseline; gap: var(--space-3); min-width: 0; } - dt { + .main-content dt { color: var(--text-faint); - font-size: 0.72rem; + font-size: var(--font-size-compact); letter-spacing: 0.08em; text-transform: uppercase; white-space: nowrap; } - dd { + .main-content dd { min-width: 0; margin: 0; } - dd small { + .main-content dd small { display: inline; margin-top: 0; margin-left: var(--space-2); } - .table-wrap { + .main-content .table-wrap { overflow-x: auto; } - table { + .main-content table { width: 100%; border-collapse: collapse; } - th, - td { + .main-content th, + .main-content td { border-bottom: 1px solid var(--line); padding: 10px 8px; text-align: left; vertical-align: top; } - th { + .main-content th { color: var(--text-faint); - font-size: 0.78rem; + font-size: var(--font-size-compact); letter-spacing: 0.08em; text-transform: uppercase; } @@ -251,8 +314,8 @@ .page-header-row h1, .workers-page-header h1 { color: var(--text-strong); - font-size: clamp(1.6rem, 4vw, 2.4rem); - line-height: 1.05; + font-size: var(--font-size-title); + line-height: var(--line-height-title); } .page-header-row p:not(.breadcrumb), .workers-page-header p { @@ -262,7 +325,7 @@ .breadcrumb { margin-bottom: var(--space-2); color: var(--text-muted); - font-size: 0.85rem; + font-size: var(--font-size-compact); } .breadcrumb a { color: inherit; diff --git a/web/workspace/src/lib/workspace/ui/Bevel.svelte b/web/workspace/src/lib/workspace/ui/Bevel.svelte new file mode 100644 index 00000000..3d8f0a93 --- /dev/null +++ b/web/workspace/src/lib/workspace/ui/Bevel.svelte @@ -0,0 +1,129 @@ + + + + {@render children()} + + + diff --git a/web/workspace/src/lib/workspace/ui/BevelLine.svelte b/web/workspace/src/lib/workspace/ui/BevelLine.svelte new file mode 100644 index 00000000..3faebcc1 --- /dev/null +++ b/web/workspace/src/lib/workspace/ui/BevelLine.svelte @@ -0,0 +1,64 @@ + + + + + diff --git a/web/workspace/src/lib/workspace/ui/Tooltip.svelte b/web/workspace/src/lib/workspace/ui/Tooltip.svelte new file mode 100644 index 00000000..87f5ab63 --- /dev/null +++ b/web/workspace/src/lib/workspace/ui/Tooltip.svelte @@ -0,0 +1,178 @@ + + + + + + {@render children(id)} + + {text} + + + + diff --git a/web/workspace/src/routes/+layout.svelte b/web/workspace/src/routes/+layout.svelte index bdd548d5..c5e0e880 100644 --- a/web/workspace/src/routes/+layout.svelte +++ b/web/workspace/src/routes/+layout.svelte @@ -2,6 +2,7 @@ import { page } from '$app/state'; import { setContext } from 'svelte'; import WorkspaceAlerts from '$lib/workspace/alerts/WorkspaceAlerts.svelte'; + import Bevel from '$lib/workspace/ui/Bevel.svelte'; import { provideHeaderController, type HeaderController } from '$lib/workspace/header/context'; import GlobalSidebar from '$lib/workspace/sidebar/GlobalSidebar.svelte'; import SidebarFrame from '$lib/workspace/sidebar/SidebarFrame.svelte'; @@ -16,6 +17,11 @@ sidebar = activeSidebar; }); const headerController = $state({ content: null }); + let sidebarFolded = $state(false); + + function toggleSidebar() { + sidebarFolded = !sidebarFolded; + } provideHeaderController(headerController); setContext(SIDEBAR_CONTEXT, { @@ -25,8 +31,8 @@ -
- +
+ -
-
- {#if headerController.content}{@render headerController.content()}{/if} -
- -
+ +
+
+ {#if headerController.content}{@render headerController.content()}{/if} +
+ +
+
{@render children()}
@@ -65,11 +91,14 @@ min-width: 0; } - .app-shell__topbar { - position: relative; + .app-shell :global(.bevel.app-shell__topbar-bevel) { z-index: 30; grid-column: 2; grid-row: 1; + min-width: 0; + } + + .app-shell__topbar { display: flex; align-items: center; justify-content: space-between; @@ -77,7 +106,6 @@ min-width: 0; min-height: 3.25rem; padding: 0 var(--space-5); - border-bottom: 1px solid var(--line); background: color-mix(in srgb, var(--bg-raised) 88%, transparent); backdrop-filter: blur(14px); } @@ -111,6 +139,14 @@ color: var(--text-muted); } + .app-shell__mobile-sidebar-toggle { + display: none; + padding: 0; + border: 0; + background: transparent; + cursor: pointer; + } + .app-shell__icon { width: 1.1rem; height: 1.1rem; @@ -138,25 +174,36 @@ @media (max-width: 760px) { .app-shell { - grid-template-columns: 1fr; - grid-template-rows: auto auto 1fr; + grid-template-columns: minmax(0, 1fr); + grid-template-rows: auto minmax(0, 1fr); width: 100vw; - height: auto; - min-height: 100dvh; - overflow: visible; + height: 100dvh; + min-height: 0; + overflow: hidden; + } + + .app-shell :global(.bevel.app-shell__topbar-bevel) { + grid-column: 1; + grid-row: 1; } .app-shell__topbar { - grid-column: 1; - grid-row: 2; padding: 0 var(--space-4); } + .app-shell__mobile-sidebar-toggle { + display: inline-flex; + } + .app-shell__main { grid-column: 1; - grid-row: 3; - overflow: visible; + grid-row: 2; + overflow-y: auto; padding: var(--space-5) var(--space-4); } + + .app-shell.sidebar-open .app-shell__main { + display: none; + } } diff --git a/web/workspace/src/routes/design-lab/workspace-web-ux/+layout.svelte b/web/workspace/src/routes/design-lab/workspace-web-ux/+layout.svelte new file mode 100644 index 00000000..06d54f23 --- /dev/null +++ b/web/workspace/src/routes/design-lab/workspace-web-ux/+layout.svelte @@ -0,0 +1,51 @@ + + +{#snippet designLabHeader()} + +{/snippet} + +{#snippet workspaceSidebar()} + +{/snippet} + + + + +{@render children()} diff --git a/web/workspace/src/routes/design-lab/workspace-web-ux/+page.svelte b/web/workspace/src/routes/design-lab/workspace-web-ux/+page.svelte new file mode 100644 index 00000000..23c7afa4 --- /dev/null +++ b/web/workspace/src/routes/design-lab/workspace-web-ux/+page.svelte @@ -0,0 +1,277 @@ + + +
+
+

Action

+ +
+ + + + + {#snippet children(descriptionId)} + + + + {/snippet} + + + + + + + + +
+ +
+ +
+ + Closed border + 3 checks passed at 14:32 + + + + +
+
+ +
+ + Read-only border + Read-only control group + + Locked +
+
+ +
+ + Top + left + Only the shared top-left corner is rounded + +
+
+ +
+ + Right + bottom + Only the shared bottom-right corner is rounded + +
+
+
+
+ +
+

Line

+ +
+
+ Horizontal · 100% + + Horizontal · 160px + +
+
+ Before + + After +
+
+
+ +
+

Status

+ +
+ Ready + Running + Needs review + Failed + Unavailable +
+
+ +
+

Feedback

+ + +
+ +
+

Resource row

+ + +
+ +
+

Comparable data

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + +
RuntimeKindState + + {#snippet children(descriptionId)} + + Last seen + {/snippet} + +
Local developmentlocalBuilt-inAvailableNow
Remote Linux runner with a long operational namerunner-eu-central-01RemoteUnavailable2 hours ago
+
+
+ +
+
+

Key-value

+ +
+
Repository
yoi
+ +
Default selector
refs/heads/main
+ +
Access
Configured
+
+
+ +
+

Tooltip

+ + + {#snippet children(descriptionId)} + + {/snippet} + +
+
+ +
+

Disclosure

+ +
+ Technical details +
+
Revision
rev_01J8WZ7C8AV6P3Y2N0M4KQ9B1F
+ +
Digest
sha256:2f8d9bb168afb7b19f447463f3fbe8b4728d8e87a64eaf1afbea1ca4d4c41f21
+
+
+
+ +
+

Color

+ +
+
--bg
+
--bg-raised
+
--bg-subtle
+
--text-strong
+
--accent
+
--success
+
--warning
+
--danger
+
+
+ +
+
+

Typography

+ +
+
Workspace identity24 / 32
+
Repository access14 / 20
+
refs/heads/main12 / 16
+
+
+ +
+

Spacing

+ +
+
4
+
8
+
12
+
16
+
24
+
32
+
+
+
+
diff --git a/web/workspace/src/routes/design-lab/workspace-web-ux/WorkspaceSidebarFixture.svelte b/web/workspace/src/routes/design-lab/workspace-web-ux/WorkspaceSidebarFixture.svelte new file mode 100644 index 00000000..ed0f2472 --- /dev/null +++ b/web/workspace/src/routes/design-lab/workspace-web-ux/WorkspaceSidebarFixture.svelte @@ -0,0 +1,94 @@ + + +
+ + + {#if content} + {@render content()} + {:else} + + {/if} +
diff --git a/web/workspace/src/routes/design-lab/workspace-web-ux/settings/+layout.svelte b/web/workspace/src/routes/design-lab/workspace-web-ux/settings/+layout.svelte new file mode 100644 index 00000000..dc789fb0 --- /dev/null +++ b/web/workspace/src/routes/design-lab/workspace-web-ux/settings/+layout.svelte @@ -0,0 +1,33 @@ + + +{#snippet settingsSidebar()} + +{/snippet} + + + +{@render children()} diff --git a/web/workspace/src/routes/design-lab/workspace-web-ux/settings/+page.svelte b/web/workspace/src/routes/design-lab/workspace-web-ux/settings/+page.svelte new file mode 100644 index 00000000..7381b081 --- /dev/null +++ b/web/workspace/src/routes/design-lab/workspace-web-ux/settings/+page.svelte @@ -0,0 +1,100 @@ + + +
+
+

Form field

+ +
+
+ + + + + +
+ + + + +
+ +
+

Operation error

+ + +
+ +
+

Permission boundary

+ +
+
+
Credential source
Workspace secret store
+ +
Access
Read-only
+
+

Workspace owner permission is required to replace repository credentials.

+
+
+ +
+

Empty

+ + +
+
diff --git a/web/workspace/src/routes/design-lab/workspace-web-ux/settings/SettingsSidebarFixture.svelte b/web/workspace/src/routes/design-lab/workspace-web-ux/settings/SettingsSidebarFixture.svelte new file mode 100644 index 00000000..ef58e3f5 --- /dev/null +++ b/web/workspace/src/routes/design-lab/workspace-web-ux/settings/SettingsSidebarFixture.svelte @@ -0,0 +1,35 @@ + + +
+ {#if content} + {@render content()} + {:else} + + {/if} +
diff --git a/web/workspace/src/routes/design-lab/workspace-web-ux/settings/settings-navigation.ts b/web/workspace/src/routes/design-lab/workspace-web-ux/settings/settings-navigation.ts new file mode 100644 index 00000000..6a71d820 --- /dev/null +++ b/web/workspace/src/routes/design-lab/workspace-web-ux/settings/settings-navigation.ts @@ -0,0 +1,12 @@ +import { designLabBasePath } from '../workspace-navigation'; + +export const settingsBasePath = `${designLabBasePath}/settings`; + +export const settingsNavigation = [ + { label: 'Runtimes', href: settingsBasePath }, + { label: 'Configuration Sources', href: `${settingsBasePath}?section=configuration-sources` }, + { label: 'Repositories', href: `${settingsBasePath}?section=repositories` }, + { label: 'Repository Access', href: `${settingsBasePath}?section=repository-access` }, + { label: 'Profile Sources', href: `${settingsBasePath}?section=profile-sources` }, + { label: 'Workspace Identity', href: `${settingsBasePath}?section=workspace-identity` }, +] as const; diff --git a/web/workspace/src/routes/design-lab/workspace-web-ux/showroom.css b/web/workspace/src/routes/design-lab/workspace-web-ux/showroom.css new file mode 100644 index 00000000..4c7d89be --- /dev/null +++ b/web/workspace/src/routes/design-lab/workspace-web-ux/showroom.css @@ -0,0 +1,683 @@ +@layer reset, tokens, base, layout, components; +@layer components { + .design-lab-header { + display: flex; + min-width: 0; + align-items: center; + gap: var(--space-2); + color: var(--text-muted); + font-size: var(--font-size-compact); + font-weight: 600; + line-height: 20px; + } + + .design-lab-header a { + color: var(--text-muted); + text-decoration: none; + } + + .design-lab-header a:hover { + color: var(--text-strong); + text-decoration: underline; + text-underline-offset: 3px; + } + + .design-lab-header [aria-current="page"] { + min-width: 0; + overflow: hidden; + color: var(--text-strong); + text-overflow: ellipsis; + white-space: nowrap; + } + + .workspace-sidebar .section-action { + margin-left: auto; + padding: var(--space-1) var(--space-2); + border-radius: var(--radius-soft); + color: var(--accent); + font-size: var(--font-size-compact); + font-weight: 650; + line-height: 16px; + text-decoration: none; + } + + .workspace-sidebar .section-action:hover, + .workspace-sidebar .section-action:focus-visible { + background: var(--interactive-hover); + } + + .showroom { + width: min(100%, 1180px); + margin: 0 auto; + padding: clamp(var(--space-5), 4vw, var(--space-6)); + display: grid; + gap: var(--space-6); + } + + .showroom--settings { + width: min(100%, 900px); + margin-inline: 0; + } + + .showroom-section { + min-width: 0; + display: grid; + align-content: start; + gap: var(--space-3); + } + + .showroom-heading { + margin: 0; + padding-bottom: var(--space-2); + color: var(--text-strong); + font-size: var(--font-size-body); + font-weight: 650; + line-height: 20px; + } + + .showroom-split { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: calc(var(--space-6) * 1.5); + } + + .action-row, + .status-row { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--space-3); + } + + .action { + min-height: 36px; + border: 1px solid transparent; + border-radius: var(--radius-soft); + padding: 7px var(--space-4); + background: transparent; + color: inherit; + font-size: var(--font-size-compact); + font-weight: 600; + line-height: 20px; + cursor: pointer; + } + + .action--primary { + background: var(--accent); + color: var(--bg); + } + + .action--primary:hover { + background: color-mix(in oklab, var(--accent), var(--text-strong) 14%); + } + + .action--secondary { + background: var(--bg-raised); + color: var(--text-strong); + } + + .action--secondary:hover { + background: var(--interactive-hover); + } + + .action--destructive { + background: color-mix(in oklab, var(--danger) 14%, var(--bg-raised)); + color: var(--danger); + } + + .action--destructive:hover { + background: color-mix(in oklab, var(--danger) 22%, var(--bg-raised)); + } + + .action--text { + padding-inline: var(--space-2); + color: var(--accent); + } + + .action--text:hover { + background: var(--interactive-hover); + color: var(--accent-muted); + } + + .action:disabled { + cursor: not-allowed; + } + + .compact-control { + min-height: 30px; + border: 1px solid transparent; + border-radius: inherit; + padding: var(--space-1) var(--space-3); + background: var(--interactive-selected); + color: var(--text-strong); + cursor: pointer; + font: 600 var(--font-size-compact) / var(--line-height-compact) + var(--font-mono); + } + + .border-matrix { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--space-4); + } + + .tactile-group { + display: flex; + min-height: 58px; + align-items: center; + justify-content: space-between; + gap: var(--space-4); + padding: var(--space-3) var(--space-4); + border-radius: inherit; + color: var(--text); + } + + .tactile-group > span { + display: grid; + gap: var(--space-1); + } + + .tactile-group small { + color: var(--text-muted); + font-family: var(--font-mono); + } + + .line-showcase { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: stretch; + gap: var(--space-6); + } + + .line-sample--horizontal { + display: grid; + align-content: center; + gap: var(--space-3); + } + + .line-sample--vertical { + display: flex; + min-height: 96px; + align-items: center; + gap: var(--space-3); + color: var(--text-muted); + font-family: var(--font-mono); + font-size: var(--font-size-compact); + } + + .line-sample__label { + color: var(--text-muted); + font-family: var(--font-mono); + font-size: var(--font-size-compact); + } + + .status { + display: inline-flex; + min-width: 0; + align-items: center; + gap: var(--space-2); + color: var(--text); + font-size: var(--font-size-compact); + font-weight: 600; + line-height: 18px; + white-space: nowrap; + } + + .status__marker { + width: 8px; + height: 8px; + flex: 0 0 auto; + border-radius: 50%; + background: var(--text-faint); + } + + .status__marker[data-tone="success"] { + background: var(--success); + } + + .status__marker[data-tone="accent"] { + background: var(--accent); + } + + .status__marker[data-tone="warning"] { + background: var(--warning); + } + + .status__marker[data-tone="danger"] { + background: var(--danger); + } + + .status__marker--loading { + border: 2px solid color-mix(in oklch, var(--accent), transparent 65%); + border-top-color: var(--accent); + background: transparent !important; + animation: showroom-spin 800ms linear infinite; + } + + @keyframes showroom-spin { + to { + transform: rotate(360deg); + } + } + + .feedback-grid { + display: grid; + gap: var(--space-2); + } + + .feedback { + min-height: 48px; + padding: var(--space-3) var(--space-4); + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-4); + border-left: 3px solid var(--line-strong); + background: var(--bg-raised); + } + + .feedback strong { + color: var(--text-strong); + font-size: var(--font-size-body); + } + + .feedback p { + margin: 2px 0 0; + color: var(--text-muted); + font-size: var(--font-size-body); + line-height: var(--line-height-body); + } + + .feedback--error { + border-left-color: var(--danger); + } + + .feedback--permission { + border-left-color: var(--warning); + } + + .resource-list { + min-width: 0; + margin-inline: var(--space-2); + } + + .resource-row { + min-width: 0; + min-height: 64px; + padding: var(--space-3) var(--space-2); + display: grid; + grid-template-columns: + minmax(0, 1fr) minmax(110px, auto) minmax(90px, auto); + align-items: center; + gap: var(--space-4); + color: inherit; + text-decoration: none; + } + + .resource-row:hover { + border-radius: var(--radius-soft); + background: var(--interactive-hover); + } + + .resource-row__identity { + min-width: 0; + display: grid; + gap: var(--space-1); + } + + .resource-row__identity strong { + min-width: 0; + overflow-wrap: anywhere; + color: var(--text-strong); + font-weight: 600; + } + + .resource-row__identity code { + min-width: 0; + overflow-wrap: anywhere; + color: var(--text-muted); + font-size: var(--font-size-compact); + line-height: 16px; + } + + .resource-row__meta { + color: var(--text-muted); + font-size: var(--font-size-compact); + line-height: 16px; + text-align: right; + } + + .table-scroll { + max-width: 100%; + overflow-x: auto; + } + + .table-scroll:focus-visible { + border-radius: var(--radius-soft); + } + + .showroom table { + width: 100%; + min-width: 680px; + border-collapse: collapse; + text-align: left; + } + + .showroom th, + .showroom td { + padding: var(--space-3) var(--space-4); + vertical-align: middle; + } + + .showroom tbody tr:nth-child(even) { + background: var(--bg-raised); + } + + .showroom thead th { + color: var(--text-muted); + font-size: var(--font-size-compact); + font-weight: 600; + line-height: 16px; + } + + .showroom tbody th { + max-width: 360px; + color: var(--text-strong); + font-size: var(--font-size-compact); + font-weight: 600; + } + + .showroom tbody th span { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .showroom tbody th code { + display: block; + margin-top: 2px; + color: var(--text-muted); + font-size: var(--font-size-compact); + font-weight: 400; + } + + .column-help { + color: inherit; + text-decoration: underline dotted; + text-underline-offset: 0.2em; + cursor: help; + } + + .key-value, + .technical-values { + margin: 0 var(--space-2); + } + + .key-value > div:not(.bevel-line), + .technical-values > div:not(.bevel-line) { + padding: var(--space-2) 0; + display: grid; + grid-template-columns: minmax(110px, 0.65fr) minmax(0, 1fr); + gap: var(--space-4); + } + + .key-value dt, + .technical-values dt { + color: var(--text-muted); + font-size: var(--font-size-compact); + font-weight: 600; + line-height: 18px; + } + + .key-value dd, + .technical-values dd { + min-width: 0; + margin: 0; + color: var(--text-strong); + font-size: var(--font-size-compact); + line-height: 18px; + } + + .technical-values code { + overflow-wrap: anywhere; + } + + .help-label { + min-height: 36px; + border: 0; + border-bottom: 1px dotted var(--line-strong); + background: transparent; + color: var(--text-strong); + font-weight: 600; + cursor: help; + } + + .help-label span { + color: var(--text-muted); + } + + .disclosure { + padding-block: var(--space-2); + } + + .disclosure summary { + padding: var(--space-3) var(--space-2); + color: var(--text-strong); + font-weight: 600; + cursor: pointer; + } + + .disclosure summary:hover { + border-radius: var(--radius-soft); + background: var(--interactive-hover); + } + + .technical-values { + padding: 0 var(--space-2) var(--space-4); + } + + .token-grid { + display: grid; + grid-template-columns: repeat(4, minmax(110px, 1fr)); + gap: var(--space-4); + } + + .color-token { + min-width: 0; + display: grid; + gap: var(--space-2); + } + + .color-token span { + height: 48px; + border: 1px solid var(--line); + background: var(--sample); + } + + .color-token code, + .type-samples code, + .space-samples code { + color: var(--text-muted); + font-size: var(--font-size-compact); + line-height: 16px; + } + + .type-samples { + display: grid; + gap: var(--space-4); + } + + .type-samples > div { + display: grid; + gap: var(--space-1); + } + + .type-route { + color: var(--text-strong); + font-size: var(--font-size-title); + font-weight: 650; + line-height: var(--line-height-title); + } + + .type-body { + color: var(--text); + font-size: var(--font-size-body); + line-height: 20px; + } + + .type-meta { + color: var(--text-muted); + font-family: var(--font-mono); + font-size: var(--font-size-compact); + line-height: 16px; + } + + .space-samples { + display: grid; + gap: var(--space-2); + } + + .space-samples > div { + display: grid; + grid-template-columns: minmax(0, 1fr) 32px; + align-items: center; + gap: var(--space-3); + } + + .space-samples span { + width: var(--size); + height: 8px; + min-width: 4px; + background: var(--accent-muted); + } + + .sample-form { + display: grid; + gap: var(--space-5); + margin-inline: var(--space-2); + } + + .form-grid { + display: grid; + gap: var(--space-5); + } + + .form-field { + display: grid; + gap: var(--space-2); + color: var(--text-strong); + font-size: var(--font-size-compact); + font-weight: 600; + } + + .form-field input, + .form-field select { + width: 100%; + min-height: 40px; + border: 1px solid transparent; + border-radius: var(--radius-soft); + padding: var(--space-2) var(--space-3); + background: var(--bg); + color: var(--text-strong); + } + + .form-field small { + color: var(--text-muted); + font-size: var(--font-size-compact); + font-weight: 400; + line-height: 16px; + } + + .field-error { + color: var(--danger); + font-size: var(--font-size-compact); + font-weight: 600; + line-height: 16px; + } + + .form-footer { + padding-top: var(--space-4); + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-4); + } + + .unsaved { + margin: 0; + display: inline-flex; + align-items: center; + gap: var(--space-2); + color: var(--text-strong); + font-size: var(--font-size-compact); + font-weight: 600; + } + + .permission-read-view { + display: grid; + gap: var(--space-3); + } + + .permission-read-view p { + margin: 0; + color: var(--text-muted); + font-size: var(--font-size-body); + } + + @media (prefers-reduced-motion: reduce) { + .status__marker--loading { + animation: none; + } + } + + @media (max-width: 760px) { + .showroom { + padding: var(--space-5) var(--space-4); + gap: var(--space-5); + } + + .showroom-split, + .line-showcase, + .border-matrix { + grid-template-columns: 1fr; + gap: var(--space-6); + } + + .resource-row { + grid-template-columns: minmax(0, 1fr) auto; + gap: var(--space-2) var(--space-3); + } + + .resource-row__identity { + grid-column: 1 / -1; + } + + .resource-row__meta { + text-align: left; + } + + .feedback { + align-items: flex-start; + flex-direction: column; + } + + .token-grid { + grid-template-columns: repeat(2, minmax(100px, 1fr)); + } + } + + @media (max-width: 420px) { + .key-value > div, + .technical-values > div { + grid-template-columns: 1fr; + gap: var(--space-1); + } + + .form-footer, + .tactile-group { + align-items: stretch; + flex-direction: column; + } + } +} diff --git a/web/workspace/src/routes/design-lab/workspace-web-ux/workspace-navigation.ts b/web/workspace/src/routes/design-lab/workspace-web-ux/workspace-navigation.ts new file mode 100644 index 00000000..258ffa94 --- /dev/null +++ b/web/workspace/src/routes/design-lab/workspace-web-ux/workspace-navigation.ts @@ -0,0 +1,37 @@ +export const designLabBasePath = '/design-lab/workspace-web-ux'; + +export type WorkspaceNavigationItem = { + label: string; + href: string; + children?: WorkspaceNavigationItem[]; +}; + +export const workspaceNavigation: WorkspaceNavigationItem[] = [ + { label: 'Tickets', href: `${designLabBasePath}?resource=tickets` }, + { label: 'Objectives', href: `${designLabBasePath}?resource=objectives` }, + { label: 'Merge Requests', href: `${designLabBasePath}?resource=merge-requests` }, + { + label: 'Memory', + href: `${designLabBasePath}?resource=memory`, + children: [ + { label: 'Document', href: `${designLabBasePath}?resource=memory-document` }, + { label: 'Staging', href: `${designLabBasePath}?resource=memory-staging` }, + ], + }, + { label: 'Workers', href: `${designLabBasePath}?resource=workers` }, +]; + +export const workspaceWorkers = [ + { + key: 'wrk-language-review', + label: 'Language review', + state: 'Running', + repository: 'yoi', + }, + { + key: 'wrk-accessibility-check', + label: 'Accessibility check with a deliberately long display name', + state: 'Idle', + repository: 'workspace-web', + }, +] as const; diff --git a/web/workspace/src/routes/w/[workspaceId]/memory/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/memory/+page.svelte index 99789aaa..d852919c 100644 --- a/web/workspace/src/routes/w/[workspaceId]/memory/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/memory/+page.svelte @@ -81,7 +81,7 @@ .memory-document-summary span { color: var(--text-muted); display: block; - font-size: 0.76rem; + font-size: var(--font-size-compact); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; @@ -90,7 +90,7 @@ .memory-document-summary strong { color: var(--text); display: block; - font-size: 0.95rem; + font-size: var(--font-size-compact); margin-top: 0.25rem; overflow-wrap: anywhere; } @@ -101,8 +101,8 @@ border-radius: 0.9rem; color: var(--text); font-family: var(--font-mono); - font-size: 0.88rem; - line-height: 1.6; + font-size: var(--font-size-body); + line-height: var(--line-height-body); margin: 1rem 0 0; overflow: auto; padding: 1rem; diff --git a/web/workspace/src/routes/w/[workspaceId]/memory/staging/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/memory/staging/+page.svelte index 1cdee6ed..f8b5847a 100644 --- a/web/workspace/src/routes/w/[workspaceId]/memory/staging/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/memory/staging/+page.svelte @@ -170,7 +170,7 @@ .staging-meta dt { color: var(--text-muted); display: block; - font-size: 0.76rem; + font-size: var(--font-size-compact); font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; @@ -179,7 +179,7 @@ .staging-summary-grid strong { color: var(--text); display: block; - font-size: 0.95rem; + font-size: var(--font-size-compact); margin-top: 0.25rem; overflow-wrap: anywhere; } @@ -206,7 +206,7 @@ .staging-entry h3 { color: var(--text-strong); - font-size: 1rem; + font-size: var(--font-size-body); margin: 0.35rem 0 0; } @@ -225,7 +225,7 @@ border-radius: 999px; color: var(--accent); display: inline-flex; - font-size: 0.72rem; + font-size: var(--font-size-compact); font-weight: 700; letter-spacing: 0.04em; padding: 0.18rem 0.55rem; diff --git a/web/workspace/src/routes/w/[workspaceId]/settings/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/settings/+page.svelte index 0dfd5a68..0fb597c7 100644 --- a/web/workspace/src/routes/w/[workspaceId]/settings/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/settings/+page.svelte @@ -384,7 +384,7 @@ .metadata-list div { display: grid; grid-template-columns: 8rem minmax(0, 1fr); gap: var(--space-3); } .metadata-list dd { margin: 0; overflow-wrap: anywhere; } .identity-bundle { display: grid; gap: var(--space-2); margin-top: var(--space-4); } - .identity-bundle textarea { width: 100%; resize: vertical; font-family: var(--font-mono); font-size: 0.75rem; } + .identity-bundle textarea { width: 100%; resize: vertical; font-family: var(--font-mono); font-size: var(--font-size-compact); } .danger-zone { display: flex; justify-content: space-between; align-items: start; gap: var(--space-4); border-top: 1px solid var(--color-danger, #b42318); } .danger-zone p { max-width: 68ch; } .danger-button { color: white; background: var(--color-danger, #b42318); border-color: var(--color-danger, #b42318); } diff --git a/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/workdirs/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/workdirs/+page.svelte index d6fed1f0..5a1c6f8d 100644 --- a/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/workdirs/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/workdirs/+page.svelte @@ -187,9 +187,9 @@ width: 2rem; height: 2rem; padding: 0; - border: 1px solid var(--border); + border: 1px solid var(--line); border-radius: 0.5rem; - background: var(--surface); + background: var(--bg-raised); color: var(--text); cursor: pointer; } diff --git a/web/workspace/src/routes/w/[workspaceId]/workers/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/workers/+page.svelte index e3a0d542..11a2de79 100644 --- a/web/workspace/src/routes/w/[workspaceId]/workers/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/workers/+page.svelte @@ -276,9 +276,9 @@ width: 2rem; height: 2rem; padding: 0; - border: 1px solid var(--border); + border: 1px solid var(--line); border-radius: 0.5rem; - background: var(--surface); + background: var(--bg-raised); color: var(--text); cursor: pointer; } diff --git a/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte b/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte index 28f3cbd3..7a7501e8 100644 --- a/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte +++ b/web/workspace/src/routes/w/[workspaceId]/workers/[workerRef]/console/+page.svelte @@ -2120,7 +2120,7 @@ color: var(--text-muted); padding: 0.42rem 0.65rem; font: inherit; - font-size: 0.7rem; + font-size: var(--font-size-compact); font-weight: 700; cursor: pointer; } @@ -2147,7 +2147,7 @@ .rewind-targets h3 { margin: 0; - font-size: 0.9rem; + font-size: var(--font-size-body); } .rewind-target-list { @@ -2209,7 +2209,7 @@ background: var(--bg-raised); color: var(--text-muted); cursor: pointer; - font-size: 0.76rem; + font-size: var(--font-size-compact); font-weight: 800; padding: 0.35rem 0.5rem; text-align: left; @@ -2240,7 +2240,7 @@ .pending-submissions { margin: 0 var(--space-3); color: var(--muted); - font-size: 0.75rem; + font-size: var(--font-size-compact); } .pending-submissions ol { @@ -2306,7 +2306,7 @@ .console-side-panel dt { color: var(--text-muted); - font-size: 0.72rem; + font-size: var(--font-size-compact); font-weight: 800; letter-spacing: 0.05em; text-transform: uppercase; @@ -2320,7 +2320,7 @@ .metadata-details { color: var(--text-muted); - font-size: 0.84rem; + font-size: var(--font-size-compact); } .metadata-details summary { @@ -2380,7 +2380,7 @@ border: 1px solid var(--line); border-radius: 999px; padding: 0.2rem 0.5rem; - font: 500 0.75rem/1.2 var(--font-mono); + font: 500 var(--font-size-compact) / var(--line-height-compact) var(--font-mono); } .composer-attachment.failed { @@ -2469,12 +2469,12 @@ gap: var(--space-2); align-items: center; color: var(--text-muted); - font-size: 0.86rem; + font-size: var(--font-size-compact); } .composer-notice { color: var(--text-muted); - font-size: 0.86rem; + font-size: var(--font-size-compact); } .composer-actions { diff --git a/web/workspace/tests/bevel-contract.test.ts b/web/workspace/tests/bevel-contract.test.ts new file mode 100644 index 00000000..86c6c20a --- /dev/null +++ b/web/workspace/tests/bevel-contract.test.ts @@ -0,0 +1,132 @@ +// @ts-nocheck +function assert(condition: unknown, message: string): asserts condition { + if (!condition) throw new Error(message); +} + +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), + ); + const appCss = await Deno.readTextFile( + new URL("../src/app.css", import.meta.url), + ); + const designLanguage = await Deno.readTextFile( + new URL( + "../../../docs/development/ui-ux/design-language.md", + import.meta.url, + ), + ); + + for ( + const contract of [ + "children: Snippet<[]>", + "top?: boolean", + "right?: boolean", + "bottom?: boolean", + "left?: boolean", + "data-invalid={invalid || undefined}", + "border-style: solid", + "border-color: var(--line-strong)", + "border-width: 1px", + "aria-invalid='true'", + "forced-colors: active", + ] + ) { + assert(source.includes(contract), `Bevel must preserve ${contract}`); + } + + 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("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-bottom-right-radius: 0px"), + "Bevel must draw only selected sides and round only corners shared by enabled sides", + ); + assert( + !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( + !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("1pxの単色borderを描くvisual wrapper") && + designLanguage.includes( + "照明、raised/inset、ridge/grooveを表現しない", + ) && + designLanguage.includes("text/content areaへ適用する"), + "The design language must define Bevel as a plain border without surface ownership", + ); +}); + +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", + import.meta.url, + ), + ); + const settingsSource = await Deno.readTextFile( + new URL( + "../src/routes/design-lab/workspace-web-ux/settings/+page.svelte", + import.meta.url, + ), + ); + const showroomCss = await Deno.readTextFile( + new URL( + "../src/routes/design-lab/workspace-web-ux/showroom.css", + import.meta.url, + ), + ); + const tactileGroupRule = showroomCss.match( + /\.tactile-group \{([\s\S]*?)\n \}/, + )?.[1]; + + assert( + workspaceSource.includes('
') && + workspaceSource.includes("right={false} bottom={false}") && + workspaceSource.includes("top={false} left={false}") && + workspaceSource.includes("Closed border") && + workspaceSource.includes("Read-only border") && + !workspaceSource.includes("profile=") && + !workspaceSource.includes("depth=") && + !workspaceSource.includes("") && + settingsSource.includes("") && + settingsSource.includes('aria-invalid="true"') && + !settingsSource.includes("profile=") && + !settingsSource.includes("depth="), + "Settings showroom must demonstrate plain and invalid borders", + ); +}); diff --git a/web/workspace/tests/bevel-line-contract.test.ts b/web/workspace/tests/bevel-line-contract.test.ts new file mode 100644 index 00000000..5e88b813 --- /dev/null +++ b/web/workspace/tests/bevel-line-contract.test.ts @@ -0,0 +1,132 @@ +// @ts-nocheck +function assert(condition: unknown, message: string): asserts condition { + if (!condition) throw new Error(message); +} + +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), + ); + + for ( + const contract of [ + "export type BevelLineDirection = 'x' | 'y'", + "length?: string", + "as?: 'span' | 'div'", + "role={decorative ? undefined : 'separator'}", + "'horizontal' : 'vertical'", + "style:width={direction === 'x' ? length : undefined}", + "style:height={direction === 'y' ? length : undefined}", + "pointer-events: none", + "forced-colors: active", + ] + ) { + assert(source.includes(contract), `BevelLine must preserve ${contract}`); + } + + assert( + 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("padding-inline") && + !source.includes("padding-block") && + !source.includes("margin-inline") && + !source.includes("margin-block"), + "BevelLine must fill its parent layer without independent endpoint spacing", + ); +}); + +Deno.test("Workspace showroom demonstrates both BevelLine directions", async () => { + const source = await Deno.readTextFile( + new URL( + "../src/routes/design-lab/workspace-web-ux/+page.svelte", + import.meta.url, + ), + ); + + assert( + source.includes('direction="x" length="100%"') && + source.includes('direction="y" length="72px"') && + source.includes('direction="x" length="160px"') && + !source.includes("depth=") && + !source.includes("weight="), + "Workspace showroom must demonstrate horizontal and vertical plain separators", + ); +}); + +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), + ); + const sidebarFrame = await Deno.readTextFile( + new URL( + "../src/lib/workspace/sidebar/SidebarFrame.svelte", + import.meta.url, + ), + ); + const sidebarCss = await Deno.readTextFile( + new URL("../src/lib/workspace/sidebar/sidebar.css", import.meta.url), + ); + const showroomCss = await Deno.readTextFile( + new URL( + "../src/routes/design-lab/workspace-web-ux/showroom.css", + import.meta.url, + ), + ); + const designLanguage = await Deno.readTextFile( + new URL( + "../../../docs/development/ui-ux/design-language.md", + import.meta.url, + ), + ); + + assert( + rootLayout.includes('class="app-shell__topbar-bevel"') && + 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("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 selected border 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 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 use shared Bevel components", + ); +}); diff --git a/web/workspace/tests/tooltip-contract.test.ts b/web/workspace/tests/tooltip-contract.test.ts new file mode 100644 index 00000000..92ddee1a --- /dev/null +++ b/web/workspace/tests/tooltip-contract.test.ts @@ -0,0 +1,59 @@ +// @ts-nocheck +function assert(condition: unknown, message: string): asserts condition { + if (!condition) throw new Error(message); +} + +Deno.test("Tooltip exposes the same contextual help to pointer and keyboard users", async () => { + const source = await Deno.readTextFile( + new URL("../src/lib/workspace/ui/Tooltip.svelte", import.meta.url), + ); + + for ( + const contract of [ + 'role="tooltip"', + "onpointerenter={showAfterDelay}", + "onfocusin={showImmediately}", + "event.key === 'Escape'", + "children: Snippet<[descriptionId: string]>", + "max-width: min(20rem, calc(100vw - var(--space-4)))", + ] + ) { + assert(source.includes(contract), `Tooltip must preserve ${contract}`); + } +}); + +Deno.test("Tooltip showroom binds help to actions and column headings", async () => { + const workspaceSource = await Deno.readTextFile( + new URL( + "../src/routes/design-lab/workspace-web-ux/+page.svelte", + import.meta.url, + ), + ); + const settingsSource = await Deno.readTextFile( + new URL( + "../src/routes/design-lab/workspace-web-ux/settings/+page.svelte", + import.meta.url, + ), + ); + + for (const descriptionId of ["rerun-help", "last-seen-help", "source-help"]) { + assert( + workspaceSource.includes(descriptionId), + `Workspace showroom must expose contextual help ${descriptionId}`, + ); + } + assert( + settingsSource.includes("retry-runtime-help"), + "Settings showroom must expose retry operation help", + ); + assert( + workspaceSource.includes('class="column-help"') && + workspaceSource.includes("aria-describedby={descriptionId}>Last seen"), + "Column help must be bound to the column label rather than a separate info button", + ); + assert( + !workspaceSource.includes("Actions explain") && + !workspaceSource.includes("Use a table when"), + "Showroom must not restore persistent operation or table explanation", + ); +}); diff --git a/web/workspace/tests/typography-contract.test.ts b/web/workspace/tests/typography-contract.test.ts new file mode 100644 index 00000000..4593a37f --- /dev/null +++ b/web/workspace/tests/typography-contract.test.ts @@ -0,0 +1,84 @@ +// @ts-nocheck +function assert(condition: unknown, message: string): asserts condition { + if (!condition) throw new Error(message); +} + +async function collectStyleSources(directory: URL): Promise { + const files: URL[] = []; + + for await (const entry of Deno.readDir(directory)) { + const url = new URL(entry.name, directory); + if (entry.isDirectory) { + url.pathname += "/"; + files.push(...await collectStyleSources(url)); + } else if ( + entry.isFile && + (entry.name.endsWith(".css") || entry.name.endsWith(".svelte")) + ) { + files.push(url); + } + } + + return files; +} + +Deno.test("Workspace typography uses only the title body and compact scales", async () => { + const sourceRoot = new URL("../src/", import.meta.url); + const allowedSizes = new Set([ + "var(--font-size-title)", + "var(--font-size-body)", + "var(--font-size-compact)", + "inherit", + ]); + + for (const url of await collectStyleSources(sourceRoot)) { + const source = await Deno.readTextFile(url); + for (const match of source.matchAll(/font-size:\s*([^;]+);/g)) { + const value = match[1].trim(); + assert( + allowedSizes.has(value), + `${url.pathname} must not use the font-size value ${value}`, + ); + } + assert( + !/font:\s*[^;]*(?:\d+(?:\.\d+)?(?:px|rem|em))[^;]*;/g.test(source), + `${url.pathname} font shorthand must use a shared typography token`, + ); + } +}); + +Deno.test("Workspace typography tokens bind the documented three-level scale", async () => { + const appCss = await Deno.readTextFile( + new URL("../src/app.css", import.meta.url), + ); + const designLanguage = await Deno.readTextFile( + new URL( + "../../../docs/development/ui-ux/design-language.md", + import.meta.url, + ), + ); + + for ( + const contract of [ + "--font-size-title: 24px", + "--line-height-title: 32px", + "--font-size-body: 14px", + "--line-height-body: 20px", + "--font-size-compact: 12px", + "--line-height-compact: 16px", + "font-size: var(--font-size-title)", + "font-size: var(--font-size-body)", + "font-size: var(--font-size-compact)", + ] + ) { + assert(appCss.includes(contract), `app.css must preserve ${contract}`); + } + + assert( + designLanguage.includes("--font-size-title /* 24px */") && + designLanguage.includes("--font-size-body /* 14px */") && + designLanguage.includes("--font-size-compact /* 12px */") && + designLanguage.includes("`12px`未満と`13px`を使わない"), + "The design language must preserve the title, body, and compact typography contract", + ); +});