feat: merge workspace web UX demo
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
+80
-132
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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<SidebarSnippet>"),
|
||||
"Workspace design-lab layout must provide a child sidebar slot",
|
||||
);
|
||||
assert(
|
||||
workspaceLayout.includes("<WorkspaceSidebarFixture"),
|
||||
"Workspace design-lab layout must register its WorkspaceSidebar fixture",
|
||||
);
|
||||
assert(
|
||||
workspaceLayout.includes("content={sidebarContent}"),
|
||||
"Workspace design-lab layout must expose its child override slot",
|
||||
);
|
||||
assert(
|
||||
!workspaceLayout.includes("<GlobalNavSections"),
|
||||
"Workspace design-lab layout must not flatten Global navigation",
|
||||
);
|
||||
assert(
|
||||
!workspaceLayout.includes("<SettingsSidebar"),
|
||||
"Workspace design-lab layout must not flatten Settings navigation",
|
||||
);
|
||||
|
||||
assert(
|
||||
settingsLayout.includes("createOverrideStack<SidebarSnippet>"),
|
||||
"Settings design-lab layout must provide a child sidebar slot",
|
||||
);
|
||||
assert(
|
||||
settingsLayout.includes("<SettingsSidebarFixture"),
|
||||
"Settings design-lab layout must register its SettingsSidebar fixture",
|
||||
);
|
||||
assert(
|
||||
settingsLayout.includes("controller={parentSidebarController}"),
|
||||
"Settings design-lab layout must register into its parent slot",
|
||||
);
|
||||
assert(
|
||||
!settingsLayout.includes("<WorkspaceSidebar"),
|
||||
"Settings design-lab layout must not duplicate WorkspaceSidebar",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("workspace Worker list lives on the dedicated Workers page", async () => {
|
||||
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("<RichMarkdown text={item.body || '—'} />") &&
|
||||
@@ -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("<SidebarFrame bind:folded={sidebarFolded}>") &&
|
||||
!workspaceLayout.includes("sidebarFolded") &&
|
||||
!workspaceLayout.includes("onToggleFold") &&
|
||||
!sidebar.includes("folded?: boolean") &&
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -1,46 +1,51 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import Bevel from '$lib/workspace/ui/Bevel.svelte';
|
||||
import './sidebar.css';
|
||||
|
||||
type Props = {
|
||||
children: Snippet<[]>;
|
||||
folded?: boolean;
|
||||
};
|
||||
|
||||
let { children }: Props = $props();
|
||||
let folded = $state(false);
|
||||
let { children, folded = $bindable(false) }: Props = $props();
|
||||
|
||||
function toggleFold() {
|
||||
folded = !folded;
|
||||
}
|
||||
</script>
|
||||
|
||||
<aside class="sidebar-frame" class:folded aria-label="Sidebar">
|
||||
{#if !folded}
|
||||
<div class="sidebar-frame-content">
|
||||
{@render children()}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="sidebar-control-row">
|
||||
<button
|
||||
class="sidebar-fold-button"
|
||||
type="button"
|
||||
aria-label={folded ? 'Unfold sidebar' : 'Fold sidebar'}
|
||||
aria-expanded={!folded}
|
||||
title={folded ? 'Unfold sidebar' : 'Fold sidebar'}
|
||||
onclick={toggleFold}
|
||||
>
|
||||
{#if folded}
|
||||
<svg class="sidebar-icon" aria-hidden="true" viewBox="0 0 24 24">
|
||||
<path d="m6 17 5-5-5-5" />
|
||||
<path d="m13 17 5-5-5-5" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg class="sidebar-icon" aria-hidden="true" viewBox="0 0 24 24">
|
||||
<path d="m11 17-5-5 5-5" />
|
||||
<path d="m18 17-5-5 5-5" />
|
||||
</svg>
|
||||
<div class="sidebar-frame" class:folded>
|
||||
<Bevel as="div" class="sidebar-frame__bevel" top={false} bottom={false} left={false} fill>
|
||||
<aside class="sidebar-frame__surface" aria-label="Sidebar">
|
||||
{#if !folded}
|
||||
<div class="sidebar-frame-content">
|
||||
{@render children()}
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="sidebar-control-row">
|
||||
<button
|
||||
class="sidebar-fold-button"
|
||||
type="button"
|
||||
aria-label={folded ? 'Unfold sidebar' : 'Fold sidebar'}
|
||||
aria-expanded={!folded}
|
||||
title={folded ? 'Unfold sidebar' : 'Fold sidebar'}
|
||||
onclick={toggleFold}
|
||||
>
|
||||
{#if folded}
|
||||
<svg class="sidebar-icon" aria-hidden="true" viewBox="0 0 24 24">
|
||||
<path d="m6 17 5-5-5-5" />
|
||||
<path d="m13 17 5-5-5-5" />
|
||||
</svg>
|
||||
{:else}
|
||||
<svg class="sidebar-icon" aria-hidden="true" viewBox="0 0 24 24">
|
||||
<path d="m11 17-5-5 5-5" />
|
||||
<path d="m18 17-5-5 5-5" />
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
</Bevel>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
type Props = {
|
||||
children: Snippet<[]>;
|
||||
top?: boolean;
|
||||
right?: boolean;
|
||||
bottom?: boolean;
|
||||
left?: boolean;
|
||||
fill?: boolean;
|
||||
invalid?: boolean;
|
||||
as?: 'span' | 'div';
|
||||
class?: string;
|
||||
};
|
||||
|
||||
let {
|
||||
children,
|
||||
top = true,
|
||||
right = true,
|
||||
bottom = true,
|
||||
left = true,
|
||||
fill = false,
|
||||
invalid = false,
|
||||
as = 'span',
|
||||
class: className = '',
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={as}
|
||||
class={`bevel ${fill ? 'bevel--fill' : ''} ${className}`.trim()}
|
||||
data-top={top ? 'true' : 'false'}
|
||||
data-right={right ? 'true' : 'false'}
|
||||
data-bottom={bottom ? 'true' : 'false'}
|
||||
data-left={left ? 'true' : 'false'}
|
||||
data-invalid={invalid || undefined}
|
||||
>
|
||||
{@render children()}
|
||||
</svelte:element>
|
||||
|
||||
<style>
|
||||
.bevel {
|
||||
--bevel-radius: var(--radius-soft);
|
||||
--bevel-top-left-radius: var(--bevel-radius);
|
||||
--bevel-top-right-radius: var(--bevel-radius);
|
||||
--bevel-bottom-right-radius: var(--bevel-radius);
|
||||
--bevel-bottom-left-radius: var(--bevel-radius);
|
||||
|
||||
display: inline-grid;
|
||||
min-width: 0;
|
||||
border-style: solid;
|
||||
border-color: var(--line-strong);
|
||||
border-width: 1px;
|
||||
border-radius:
|
||||
var(--bevel-top-left-radius)
|
||||
var(--bevel-top-right-radius)
|
||||
var(--bevel-bottom-right-radius)
|
||||
var(--bevel-bottom-left-radius);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.bevel[data-top='false'] {
|
||||
--bevel-top-left-radius: 0px;
|
||||
--bevel-top-right-radius: 0px;
|
||||
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
.bevel[data-right='false'] {
|
||||
--bevel-top-right-radius: 0px;
|
||||
--bevel-bottom-right-radius: 0px;
|
||||
|
||||
border-right-width: 0;
|
||||
}
|
||||
|
||||
.bevel[data-bottom='false'] {
|
||||
--bevel-bottom-right-radius: 0px;
|
||||
--bevel-bottom-left-radius: 0px;
|
||||
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
|
||||
.bevel[data-left='false'] {
|
||||
--bevel-bottom-left-radius: 0px;
|
||||
--bevel-top-left-radius: 0px;
|
||||
|
||||
border-left-width: 0;
|
||||
}
|
||||
|
||||
.bevel--fill {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bevel > :global(*) {
|
||||
min-width: 0;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.bevel > :global(:is(button, input, select, textarea)) {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.bevel[data-invalid='true'],
|
||||
.bevel:has(> :global([aria-invalid='true'])) {
|
||||
border-color: var(--danger);
|
||||
}
|
||||
|
||||
.bevel:has(> :global(:is(:disabled, [aria-disabled='true']))) {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
.bevel {
|
||||
border-color: ButtonText;
|
||||
forced-color-adjust: auto;
|
||||
}
|
||||
|
||||
.bevel[data-invalid='true'],
|
||||
.bevel:has(> :global([aria-invalid='true'])) {
|
||||
border-color: Mark;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.bevel {
|
||||
border-color: currentColor;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
export type BevelLineDirection = 'x' | 'y';
|
||||
|
||||
type Props = {
|
||||
direction?: BevelLineDirection;
|
||||
length?: string;
|
||||
decorative?: boolean;
|
||||
as?: 'span' | 'div';
|
||||
class?: string;
|
||||
};
|
||||
|
||||
let {
|
||||
direction = 'x',
|
||||
length = '100%',
|
||||
decorative = false,
|
||||
as = 'span',
|
||||
class: className = '',
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<svelte:element
|
||||
this={as}
|
||||
class={`bevel-line ${className}`.trim()}
|
||||
data-direction={direction}
|
||||
role={decorative ? undefined : 'separator'}
|
||||
aria-orientation={decorative ? undefined : direction === 'x' ? 'horizontal' : 'vertical'}
|
||||
aria-hidden={decorative ? 'true' : undefined}
|
||||
style:width={direction === 'x' ? length : undefined}
|
||||
style:height={direction === 'y' ? length : undefined}
|
||||
></svelte:element>
|
||||
|
||||
<style>
|
||||
.bevel-line {
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
border-color: var(--line-strong);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bevel-line[data-direction='x'] {
|
||||
height: 0;
|
||||
border-top-style: solid;
|
||||
border-top-width: 1px;
|
||||
}
|
||||
|
||||
.bevel-line[data-direction='y'] {
|
||||
width: 0;
|
||||
border-left-style: solid;
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
.bevel-line {
|
||||
border-color: CanvasText;
|
||||
forced-color-adjust: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.bevel-line {
|
||||
border-color: currentColor;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,178 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount, tick, type Snippet } from 'svelte';
|
||||
|
||||
type Placement = 'top' | 'bottom';
|
||||
type Alignment = 'start' | 'center' | 'end';
|
||||
|
||||
let {
|
||||
id,
|
||||
text,
|
||||
placement = 'top',
|
||||
align = 'center',
|
||||
children,
|
||||
}: {
|
||||
id: string;
|
||||
text: string;
|
||||
placement?: Placement;
|
||||
align?: Alignment;
|
||||
children: Snippet<[descriptionId: string]>;
|
||||
} = $props();
|
||||
|
||||
let open = $state(false);
|
||||
let pointerInside = false;
|
||||
let focusInside = false;
|
||||
let hoverTimer: number | undefined;
|
||||
let rootElement: HTMLSpanElement | undefined;
|
||||
let tooltipElement: HTMLSpanElement | undefined;
|
||||
let tooltipLeft = $state(0);
|
||||
let tooltipTop = $state(0);
|
||||
|
||||
function clearHoverTimer() {
|
||||
if (hoverTimer === undefined) return;
|
||||
globalThis.clearTimeout(hoverTimer);
|
||||
hoverTimer = undefined;
|
||||
}
|
||||
|
||||
function positionTooltip() {
|
||||
if (!open || rootElement === undefined || tooltipElement === undefined) return;
|
||||
|
||||
const root = rootElement.getBoundingClientRect();
|
||||
const tooltip = tooltipElement.getBoundingClientRect();
|
||||
const margin = 8;
|
||||
const gap = 8;
|
||||
|
||||
let left = root.left + (root.width - tooltip.width) / 2;
|
||||
if (align === 'start') left = root.left;
|
||||
if (align === 'end') left = root.right - tooltip.width;
|
||||
left = Math.max(margin, Math.min(left, globalThis.innerWidth - tooltip.width - margin));
|
||||
|
||||
let top = placement === 'top' ? root.top - tooltip.height - gap : root.bottom + gap;
|
||||
if (top < margin) top = root.bottom + gap;
|
||||
if (top + tooltip.height > globalThis.innerHeight - margin) {
|
||||
top = root.top - tooltip.height - gap;
|
||||
}
|
||||
top = Math.max(margin, Math.min(top, globalThis.innerHeight - tooltip.height - margin));
|
||||
|
||||
tooltipLeft = Math.round(left);
|
||||
tooltipTop = Math.round(top);
|
||||
}
|
||||
|
||||
function reveal() {
|
||||
open = true;
|
||||
void tick().then(positionTooltip);
|
||||
}
|
||||
|
||||
function showAfterDelay() {
|
||||
pointerInside = true;
|
||||
clearHoverTimer();
|
||||
hoverTimer = globalThis.setTimeout(() => {
|
||||
reveal();
|
||||
hoverTimer = undefined;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function showImmediately() {
|
||||
focusInside = true;
|
||||
clearHoverTimer();
|
||||
reveal();
|
||||
}
|
||||
|
||||
function dismiss() {
|
||||
clearHoverTimer();
|
||||
open = false;
|
||||
}
|
||||
|
||||
function handlePointerLeave() {
|
||||
pointerInside = false;
|
||||
clearHoverTimer();
|
||||
if (!focusInside) open = false;
|
||||
}
|
||||
|
||||
function handleFocusOut(event: FocusEvent) {
|
||||
const root = event.currentTarget;
|
||||
if (root instanceof HTMLElement && event.relatedTarget instanceof Node && root.contains(event.relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
focusInside = false;
|
||||
if (!pointerInside) open = false;
|
||||
}
|
||||
|
||||
function handleWindowKeydown(event: KeyboardEvent) {
|
||||
if (open && event.key === 'Escape') dismiss();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const reposition = () => positionTooltip();
|
||||
globalThis.addEventListener('resize', reposition);
|
||||
document.addEventListener('scroll', reposition, true);
|
||||
return () => {
|
||||
globalThis.removeEventListener('resize', reposition);
|
||||
document.removeEventListener('scroll', reposition, true);
|
||||
};
|
||||
});
|
||||
|
||||
onDestroy(clearHoverTimer);
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleWindowKeydown} />
|
||||
|
||||
<span
|
||||
class="tooltip-root"
|
||||
bind:this={rootElement}
|
||||
onpointerenter={showAfterDelay}
|
||||
onpointerleave={handlePointerLeave}
|
||||
onfocusin={showImmediately}
|
||||
onfocusout={handleFocusOut}
|
||||
>
|
||||
{@render children(id)}
|
||||
<span
|
||||
{id}
|
||||
class:open
|
||||
class="tooltip-content"
|
||||
role="tooltip"
|
||||
bind:this={tooltipElement}
|
||||
style:left={`${tooltipLeft}px`}
|
||||
style:top={`${tooltipTop}px`}
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.tooltip-root {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.tooltip-content {
|
||||
position: fixed;
|
||||
z-index: 50;
|
||||
width: max-content;
|
||||
max-width: min(20rem, calc(100vw - var(--space-4)));
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: var(--radius-soft);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: var(--text-strong);
|
||||
box-shadow: var(--shadow-overlay);
|
||||
color: var(--bg);
|
||||
font-family: var(--font-sans);
|
||||
font-size: var(--font-size-compact);
|
||||
font-weight: 500;
|
||||
line-height: 16px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
text-align: left;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.tooltip-content.open {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.tooltip-content {
|
||||
transition: opacity 100ms ease;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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<HeaderController>({ content: null });
|
||||
let sidebarFolded = $state(false);
|
||||
|
||||
function toggleSidebar() {
|
||||
sidebarFolded = !sidebarFolded;
|
||||
}
|
||||
|
||||
provideHeaderController(headerController);
|
||||
setContext<SidebarController>(SIDEBAR_CONTEXT, {
|
||||
@@ -25,8 +31,8 @@
|
||||
|
||||
<WorkspaceAlerts />
|
||||
|
||||
<div class="app-shell">
|
||||
<SidebarFrame>
|
||||
<div class="app-shell" class:sidebar-open={!sidebarFolded}>
|
||||
<SidebarFrame bind:folded={sidebarFolded}>
|
||||
<GlobalSidebar
|
||||
currentPath={page.url.pathname}
|
||||
content={sidebar}
|
||||
@@ -34,19 +40,39 @@
|
||||
workspaceError={data.workspaceCatalogError}
|
||||
/>
|
||||
</SidebarFrame>
|
||||
<header class="app-shell__topbar">
|
||||
<div class="app-shell__topbar-location">
|
||||
{#if headerController.content}{@render headerController.content()}{/if}
|
||||
</div>
|
||||
<nav class="app-shell__topbar-actions" aria-label="Global navigation">
|
||||
<a class="app-shell__icon-button" href="/account" aria-label="Open Account" title="Account">
|
||||
<svg class="app-shell__icon" aria-hidden="true" viewBox="0 0 24 24">
|
||||
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
<Bevel as="div" class="app-shell__topbar-bevel" top={false} right={false} left={false} fill>
|
||||
<header class="app-shell__topbar">
|
||||
<div class="app-shell__topbar-location">
|
||||
{#if headerController.content}{@render headerController.content()}{/if}
|
||||
</div>
|
||||
<nav class="app-shell__topbar-actions" aria-label="Global navigation">
|
||||
<button
|
||||
class="app-shell__icon-button app-shell__mobile-sidebar-toggle"
|
||||
type="button"
|
||||
aria-label={sidebarFolded ? 'Show sidebar' : 'Hide sidebar'}
|
||||
aria-expanded={!sidebarFolded}
|
||||
title={sidebarFolded ? 'Show sidebar' : 'Hide sidebar'}
|
||||
onclick={toggleSidebar}
|
||||
>
|
||||
<svg class="app-shell__icon" aria-hidden="true" viewBox="0 0 24 24">
|
||||
{#if sidebarFolded}
|
||||
<path d="m6 17 5-5-5-5" />
|
||||
<path d="m13 17 5-5-5-5" />
|
||||
{:else}
|
||||
<path d="m11 17-5-5 5-5" />
|
||||
<path d="m18 17-5-5 5-5" />
|
||||
{/if}
|
||||
</svg>
|
||||
</button>
|
||||
<a class="app-shell__icon-button" href="/account" aria-label="Open Account" title="Account">
|
||||
<svg class="app-shell__icon" aria-hidden="true" viewBox="0 0 24 24">
|
||||
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
</Bevel>
|
||||
<main class="app-shell__main">
|
||||
{@render children()}
|
||||
</main>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { setContext } from 'svelte';
|
||||
import HeaderOverride from '$lib/workspace/header/HeaderOverride.svelte';
|
||||
import SidebarOverride from '$lib/workspace/sidebar/SidebarOverride.svelte';
|
||||
import {
|
||||
getSidebarController,
|
||||
SIDEBAR_CONTEXT,
|
||||
type SidebarController,
|
||||
type SidebarSnippet,
|
||||
} from '$lib/workspace/sidebar/context';
|
||||
import { createOverrideStack } from '$lib/workspace/sidebar/override-stack';
|
||||
import WorkspaceSidebarFixture from './WorkspaceSidebarFixture.svelte';
|
||||
import { designLabBasePath } from './workspace-navigation';
|
||||
import './showroom.css';
|
||||
import type { LayoutProps } from './$types';
|
||||
|
||||
let { children }: LayoutProps = $props();
|
||||
const parentSidebarController = getSidebarController();
|
||||
let sidebarContent = $state<SidebarSnippet | null>(null);
|
||||
const sidebarContentOverrides = createOverrideStack<SidebarSnippet>((activeContent) => {
|
||||
sidebarContent = activeContent;
|
||||
});
|
||||
|
||||
setContext<SidebarController>(SIDEBAR_CONTEXT, {
|
||||
registerSidebar: sidebarContentOverrides.register,
|
||||
});
|
||||
|
||||
const inSettings = $derived(page.url.pathname.startsWith(`${designLabBasePath}/settings`));
|
||||
</script>
|
||||
|
||||
{#snippet designLabHeader()}
|
||||
<nav class="design-lab-header" aria-label="Current location">
|
||||
{#if inSettings}
|
||||
<a href={designLabBasePath}>Workspace Web UX</a>
|
||||
<span aria-hidden="true">/</span>
|
||||
<span aria-current="page">Settings</span>
|
||||
{:else}
|
||||
<span aria-current="page">Workspace Web UX</span>
|
||||
{/if}
|
||||
</nav>
|
||||
{/snippet}
|
||||
|
||||
{#snippet workspaceSidebar()}
|
||||
<WorkspaceSidebarFixture currentPath={page.url.pathname} content={sidebarContent} />
|
||||
{/snippet}
|
||||
|
||||
<HeaderOverride content={designLabHeader} />
|
||||
<SidebarOverride controller={parentSidebarController} sidebar={workspaceSidebar} />
|
||||
|
||||
{@render children()}
|
||||
@@ -0,0 +1,277 @@
|
||||
<script lang="ts">
|
||||
import Bevel from '$lib/workspace/ui/Bevel.svelte';
|
||||
import BevelLine from '$lib/workspace/ui/BevelLine.svelte';
|
||||
import Tooltip from '$lib/workspace/ui/Tooltip.svelte';
|
||||
</script>
|
||||
|
||||
<div class="showroom">
|
||||
<section class="showroom-section" aria-labelledby="action-heading">
|
||||
<h1 id="action-heading" class="showroom-heading">Action</h1>
|
||||
<BevelLine direction="x" length="100%" decorative />
|
||||
<div class="action-row">
|
||||
<Bevel>
|
||||
<button class="action action--primary" type="button">Create Worker</button>
|
||||
</Bevel>
|
||||
<Tooltip id="rerun-help" text="Runs validation against the current Workspace configuration." placement="bottom" align="start">
|
||||
{#snippet children(descriptionId)}
|
||||
<Bevel>
|
||||
<button class="action action--secondary" type="button" aria-describedby={descriptionId}>Re-run checks</button>
|
||||
</Bevel>
|
||||
{/snippet}
|
||||
</Tooltip>
|
||||
<button class="action action--text" type="button">View log</button>
|
||||
<Bevel>
|
||||
<button class="action action--destructive" type="button">Remove repository</button>
|
||||
</Bevel>
|
||||
<Bevel>
|
||||
<button class="action action--secondary" type="button" disabled>Running checks…</button>
|
||||
</Bevel>
|
||||
</div>
|
||||
|
||||
<div class="border-matrix">
|
||||
<Bevel as="div" fill>
|
||||
<div class="tactile-group" role="group" aria-label="Bordered validation controls">
|
||||
<span>
|
||||
<strong>Closed border</strong>
|
||||
<small>3 checks passed at 14:32</small>
|
||||
</span>
|
||||
<Bevel>
|
||||
<button class="compact-control" type="button" aria-pressed="true">Live</button>
|
||||
</Bevel>
|
||||
</div>
|
||||
</Bevel>
|
||||
<Bevel as="div" fill>
|
||||
<div class="tactile-group" role="group" aria-label="Read-only bordered controls">
|
||||
<span>
|
||||
<strong>Read-only border</strong>
|
||||
<small>Read-only control group</small>
|
||||
</span>
|
||||
<span class="status"><span class="status__marker" data-tone="muted"></span>Locked</span>
|
||||
</div>
|
||||
</Bevel>
|
||||
<Bevel as="div" right={false} bottom={false} fill>
|
||||
<div class="tactile-group" role="group" aria-label="Top and left edge">
|
||||
<span>
|
||||
<strong>Top + left</strong>
|
||||
<small>Only the shared top-left corner is rounded</small>
|
||||
</span>
|
||||
</div>
|
||||
</Bevel>
|
||||
<Bevel as="div" top={false} left={false} fill>
|
||||
<div class="tactile-group" role="group" aria-label="Right and bottom groove">
|
||||
<span>
|
||||
<strong>Right + bottom</strong>
|
||||
<small>Only the shared bottom-right corner is rounded</small>
|
||||
</span>
|
||||
</div>
|
||||
</Bevel>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="showroom-section" aria-labelledby="line-heading">
|
||||
<h2 id="line-heading" class="showroom-heading">Line</h2>
|
||||
<BevelLine direction="x" length="100%" decorative />
|
||||
<div class="line-showcase">
|
||||
<div class="line-sample line-sample--horizontal">
|
||||
<span class="line-sample__label">Horizontal · 100%</span>
|
||||
<BevelLine direction="x" length="100%" />
|
||||
<span class="line-sample__label">Horizontal · 160px</span>
|
||||
<BevelLine direction="x" length="160px" />
|
||||
</div>
|
||||
<div class="line-sample line-sample--vertical">
|
||||
<span>Before</span>
|
||||
<BevelLine direction="y" length="72px" />
|
||||
<span>After</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="showroom-section" aria-labelledby="status-heading">
|
||||
<h2 id="status-heading" class="showroom-heading">Status</h2>
|
||||
<BevelLine direction="x" length="100%" decorative />
|
||||
<div class="status-row">
|
||||
<span class="status"><span class="status__marker" data-tone="success"></span>Ready</span>
|
||||
<span class="status"><span class="status__marker" data-tone="accent"></span>Running</span>
|
||||
<span class="status"><span class="status__marker" data-tone="warning"></span>Needs review</span>
|
||||
<span class="status"><span class="status__marker" data-tone="danger"></span>Failed</span>
|
||||
<span class="status"><span class="status__marker" data-tone="muted"></span>Unavailable</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="showroom-section" aria-labelledby="feedback-heading">
|
||||
<h2 id="feedback-heading" class="showroom-heading">Feedback</h2>
|
||||
<BevelLine direction="x" length="100%" decorative />
|
||||
<div class="feedback-grid">
|
||||
<div class="feedback" role="status">
|
||||
<span class="status"><span class="status__marker status__marker--loading" data-tone="accent"></span>Loading Merge Requests…</span>
|
||||
</div>
|
||||
<div class="feedback">
|
||||
<strong>No Objectives.</strong>
|
||||
</div>
|
||||
<div class="feedback feedback--error" role="alert">
|
||||
<div>
|
||||
<strong>Merge failed.</strong>
|
||||
<p>Target branch changed after review. Refresh the target before retrying.</p>
|
||||
</div>
|
||||
<Bevel>
|
||||
<button class="action action--secondary" type="button">Refresh target</button>
|
||||
</Bevel>
|
||||
</div>
|
||||
<div class="feedback feedback--permission">
|
||||
<div>
|
||||
<strong>Repository Access is read-only.</strong>
|
||||
<p>Workspace owner permission is required to change credentials.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="resource-row" class="showroom-section" aria-labelledby="resource-row-heading">
|
||||
<h2 id="resource-row-heading" class="showroom-heading">Resource row</h2>
|
||||
<BevelLine direction="x" length="100%" decorative />
|
||||
<div class="resource-list">
|
||||
<a class="resource-row" href="/design-lab/workspace-web-ux?ticket=YOI-248">
|
||||
<span class="resource-row__identity">
|
||||
<code>YOI-248</code>
|
||||
<strong>Preserve nested navigation ownership across route transitions</strong>
|
||||
</span>
|
||||
<span class="resource-row__meta">High priority</span>
|
||||
<span class="status"><span class="status__marker" data-tone="warning"></span>Blocked</span>
|
||||
</a>
|
||||
<BevelLine direction="x" length="100%" decorative />
|
||||
<a class="resource-row" href="/design-lab/workspace-web-ux?ticket=YOI-251">
|
||||
<span class="resource-row__identity">
|
||||
<code>YOI-251</code>
|
||||
<strong>Validate repository access projection</strong>
|
||||
</span>
|
||||
<span class="resource-row__meta">Updated 14 min ago</span>
|
||||
<span class="status"><span class="status__marker" data-tone="success"></span>Ready</span>
|
||||
</a>
|
||||
<BevelLine direction="x" length="100%" decorative />
|
||||
<a class="resource-row" href="/design-lab/workspace-web-ux?ticket=YOI-252">
|
||||
<span class="resource-row__identity">
|
||||
<code>YOI-252-WITH-A-LONG-CANONICAL-RESOURCE-KEY</code>
|
||||
<strong>Keep very long translated resource names readable without pushing the current state out of the row</strong>
|
||||
</span>
|
||||
<span class="resource-row__meta">No owner</span>
|
||||
<span class="status"><span class="status__marker" data-tone="muted"></span>Draft</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="showroom-section" aria-labelledby="comparable-heading">
|
||||
<h2 id="comparable-heading" class="showroom-heading">Comparable data</h2>
|
||||
<BevelLine direction="x" length="100%" decorative />
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex (Scrollable comparison must be keyboard-focusable.) -->
|
||||
<div class="table-scroll" tabindex="0" role="region" aria-label="Runtime comparison">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Runtime</th>
|
||||
<th scope="col">Kind</th>
|
||||
<th scope="col">State</th>
|
||||
<th scope="col">
|
||||
<Tooltip id="last-seen-help" text="Most recent observation from the Runtime host." placement="top" align="end">
|
||||
{#snippet children(descriptionId)}
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex (Focusable column label exposes contextual help to keyboard users.) -->
|
||||
<span class="column-help" tabindex="0" aria-describedby={descriptionId}>Last seen</span>
|
||||
{/snippet}
|
||||
</Tooltip>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row"><span>Local development</span><code>local</code></th>
|
||||
<td>Built-in</td>
|
||||
<td><span class="status"><span class="status__marker" data-tone="success"></span>Available</span></td>
|
||||
<td>Now</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><span>Remote Linux runner with a long operational name</span><code>runner-eu-central-01</code></th>
|
||||
<td>Remote</td>
|
||||
<td><span class="status"><span class="status__marker" data-tone="danger"></span>Unavailable</span></td>
|
||||
<td>2 hours ago</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="showroom-split">
|
||||
<section class="showroom-section" aria-labelledby="key-value-heading">
|
||||
<h2 id="key-value-heading" class="showroom-heading">Key-value</h2>
|
||||
<BevelLine direction="x" length="100%" decorative />
|
||||
<dl class="key-value">
|
||||
<div><dt>Repository</dt><dd>yoi</dd></div>
|
||||
<BevelLine as="div" direction="x" length="100%" decorative />
|
||||
<div><dt>Default selector</dt><dd><code>refs/heads/main</code></dd></div>
|
||||
<BevelLine as="div" direction="x" length="100%" decorative />
|
||||
<div><dt>Access</dt><dd><span class="status"><span class="status__marker" data-tone="success"></span>Configured</span></dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="showroom-section" aria-labelledby="tooltip-heading">
|
||||
<h2 id="tooltip-heading" class="showroom-heading">Tooltip</h2>
|
||||
<BevelLine direction="x" length="100%" decorative />
|
||||
<Tooltip id="source-help" text="The source that currently supplies this configuration value." placement="bottom" align="start">
|
||||
{#snippet children(descriptionId)}
|
||||
<button class="help-label" type="button" aria-describedby={descriptionId}>Configuration source <span aria-hidden="true">?</span></button>
|
||||
{/snippet}
|
||||
</Tooltip>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="showroom-section" aria-labelledby="disclosure-heading">
|
||||
<h2 id="disclosure-heading" class="showroom-heading">Disclosure</h2>
|
||||
<BevelLine direction="x" length="100%" decorative />
|
||||
<details class="disclosure">
|
||||
<summary>Technical details</summary>
|
||||
<dl class="technical-values">
|
||||
<div><dt>Revision</dt><dd><code>rev_01J8WZ7C8AV6P3Y2N0M4KQ9B1F</code></dd></div>
|
||||
<BevelLine as="div" direction="x" length="100%" decorative />
|
||||
<div><dt>Digest</dt><dd><code>sha256:2f8d9bb168afb7b19f447463f3fbe8b4728d8e87a64eaf1afbea1ca4d4c41f21</code></dd></div>
|
||||
</dl>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="showroom-section" aria-labelledby="color-heading">
|
||||
<h2 id="color-heading" class="showroom-heading">Color</h2>
|
||||
<BevelLine direction="x" length="100%" decorative />
|
||||
<div class="token-grid">
|
||||
<div class="color-token"><span style="--sample: var(--bg)"></span><code>--bg</code></div>
|
||||
<div class="color-token"><span style="--sample: var(--bg-raised)"></span><code>--bg-raised</code></div>
|
||||
<div class="color-token"><span style="--sample: var(--bg-subtle)"></span><code>--bg-subtle</code></div>
|
||||
<div class="color-token"><span style="--sample: var(--text-strong)"></span><code>--text-strong</code></div>
|
||||
<div class="color-token"><span style="--sample: var(--accent)"></span><code>--accent</code></div>
|
||||
<div class="color-token"><span style="--sample: var(--success)"></span><code>--success</code></div>
|
||||
<div class="color-token"><span style="--sample: var(--warning)"></span><code>--warning</code></div>
|
||||
<div class="color-token"><span style="--sample: var(--danger)"></span><code>--danger</code></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="showroom-split">
|
||||
<section class="showroom-section" aria-labelledby="typography-heading">
|
||||
<h2 id="typography-heading" class="showroom-heading">Typography</h2>
|
||||
<BevelLine direction="x" length="100%" decorative />
|
||||
<div class="type-samples">
|
||||
<div><span class="type-route">Workspace identity</span><code>24 / 32</code></div>
|
||||
<div><span class="type-body">Repository access</span><code>14 / 20</code></div>
|
||||
<div><span class="type-meta">refs/heads/main</span><code>12 / 16</code></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="showroom-section" aria-labelledby="spacing-heading">
|
||||
<h2 id="spacing-heading" class="showroom-heading">Spacing</h2>
|
||||
<BevelLine direction="x" length="100%" decorative />
|
||||
<div class="space-samples">
|
||||
<div><span style="--size: var(--space-1)"></span><code>4</code></div>
|
||||
<div><span style="--size: var(--space-2)"></span><code>8</code></div>
|
||||
<div><span style="--size: var(--space-3)"></span><code>12</code></div>
|
||||
<div><span style="--size: var(--space-4)"></span><code>16</code></div>
|
||||
<div><span style="--size: var(--space-5)"></span><code>24</code></div>
|
||||
<div><span style="--size: var(--space-6)"></span><code>32</code></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,94 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { designLabBasePath, workspaceNavigation, workspaceWorkers } from './workspace-navigation';
|
||||
|
||||
let {
|
||||
currentPath,
|
||||
content = null,
|
||||
}: {
|
||||
currentPath: string;
|
||||
content?: Snippet<[]> | null;
|
||||
} = $props();
|
||||
|
||||
const settingsPath = `${designLabBasePath}/settings`;
|
||||
</script>
|
||||
|
||||
<div class="workspace-sidebar">
|
||||
<header class="sidebar-header">
|
||||
<nav class="workspace-sidebar-shortcuts" aria-label="Workspace shortcuts">
|
||||
<a
|
||||
class="workspace-sidebar-shortcut"
|
||||
class:active={currentPath === designLabBasePath}
|
||||
href={designLabBasePath}
|
||||
aria-label="Workspace home"
|
||||
title="Workspace home"
|
||||
aria-current={currentPath === designLabBasePath ? 'page' : undefined}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="m3 11 9-8 9 8"></path>
|
||||
<path d="M5 10v10h14V10"></path>
|
||||
<path d="M9 20v-6h6v6"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class="workspace-sidebar-shortcut"
|
||||
class:active={currentPath.startsWith(settingsPath)}
|
||||
href={settingsPath}
|
||||
aria-label="Workspace settings"
|
||||
title="Workspace settings"
|
||||
aria-current={currentPath === settingsPath ? 'page' : undefined}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M4 5h16M4 12h16M4 19h16"></path>
|
||||
<path d="M8 3v4M16 10v4M10 17v4"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{#if content}
|
||||
{@render content()}
|
||||
{:else}
|
||||
<nav class="sidebar-sections" aria-label="Workspace sections">
|
||||
{#each workspaceNavigation as item}
|
||||
{#if item.children}
|
||||
<section class="sidebar-nav-section sidebar-nav-section--category">
|
||||
<h2 class="sidebar-nav-section__header">{item.label}</h2>
|
||||
{#each item.children as child}
|
||||
<a class="sidebar-link" href={child.href}>{child.label}</a>
|
||||
{/each}
|
||||
</section>
|
||||
{:else if item.label === 'Workers'}
|
||||
<section class="sidebar-nav-section" aria-labelledby="design-lab-workers-heading">
|
||||
<div class="section-heading-row">
|
||||
<h2 id="design-lab-workers-heading">
|
||||
<a class="section-heading-link" href={item.href}>workers</a>
|
||||
</h2>
|
||||
<a class="section-action" href={`${designLabBasePath}?worker=new`}>New</a>
|
||||
<span class="section-count">{workspaceWorkers.length}</span>
|
||||
</div>
|
||||
<ul class="nav-list" aria-label="Workers">
|
||||
{#each workspaceWorkers as worker}
|
||||
<li class="worker-nav-item">
|
||||
<a class="worker-nav-link" href={`${designLabBasePath}?worker=${worker.key}`}>
|
||||
<span class="worker-status-indicator" aria-hidden="true">
|
||||
<span class="worker-status-dot"></span>
|
||||
</span>
|
||||
<span class="worker-nav-label" title={worker.label}>{worker.label}</span>
|
||||
<small class="worker-nav-meta" title={`${worker.state} · ${worker.repository}`}>
|
||||
{worker.state} · {worker.repository}
|
||||
</small>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
{:else}
|
||||
<section class="sidebar-nav-section sidebar-nav-section--resource">
|
||||
<a class="sidebar-link" href={item.href}>{item.label}</a>
|
||||
</section>
|
||||
{/if}
|
||||
{/each}
|
||||
</nav>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { setContext } from 'svelte';
|
||||
import SidebarOverride from '$lib/workspace/sidebar/SidebarOverride.svelte';
|
||||
import {
|
||||
getSidebarController,
|
||||
SIDEBAR_CONTEXT,
|
||||
type SidebarController,
|
||||
type SidebarSnippet,
|
||||
} from '$lib/workspace/sidebar/context';
|
||||
import { createOverrideStack } from '$lib/workspace/sidebar/override-stack';
|
||||
import SettingsSidebarFixture from './SettingsSidebarFixture.svelte';
|
||||
import type { LayoutProps } from './$types';
|
||||
|
||||
let { children }: LayoutProps = $props();
|
||||
const parentSidebarController = getSidebarController();
|
||||
let sidebarContent = $state<SidebarSnippet | null>(null);
|
||||
const sidebarContentOverrides = createOverrideStack<SidebarSnippet>((activeContent) => {
|
||||
sidebarContent = activeContent;
|
||||
});
|
||||
|
||||
setContext<SidebarController>(SIDEBAR_CONTEXT, {
|
||||
registerSidebar: sidebarContentOverrides.register,
|
||||
});
|
||||
</script>
|
||||
|
||||
{#snippet settingsSidebar()}
|
||||
<SettingsSidebarFixture currentPath={page.url.pathname} content={sidebarContent} />
|
||||
{/snippet}
|
||||
|
||||
<SidebarOverride controller={parentSidebarController} sidebar={settingsSidebar} />
|
||||
|
||||
{@render children()}
|
||||
@@ -0,0 +1,100 @@
|
||||
<script lang="ts">
|
||||
import Bevel from '$lib/workspace/ui/Bevel.svelte';
|
||||
import BevelLine from '$lib/workspace/ui/BevelLine.svelte';
|
||||
import Tooltip from '$lib/workspace/ui/Tooltip.svelte';
|
||||
|
||||
function keepSampleFormLocal(event: SubmitEvent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="showroom showroom--settings">
|
||||
<section class="showroom-section" aria-labelledby="form-field-heading">
|
||||
<h1 id="form-field-heading" class="showroom-heading">Form field</h1>
|
||||
<BevelLine direction="x" length="100%" decorative />
|
||||
<form class="sample-form" onsubmit={keepSampleFormLocal}>
|
||||
<div class="form-grid">
|
||||
<label class="form-field">
|
||||
<span>Runtime label</span>
|
||||
<Bevel fill>
|
||||
<input name="runtime-label" value="Remote Linux runner" />
|
||||
</Bevel>
|
||||
</label>
|
||||
|
||||
<label class="form-field">
|
||||
<span>Endpoint</span>
|
||||
<Bevel fill invalid>
|
||||
<input
|
||||
name="runtime-endpoint"
|
||||
value="http://runner.internal.example:8787/api/runtime"
|
||||
aria-describedby="endpoint-constraint endpoint-error"
|
||||
aria-invalid="true"
|
||||
/>
|
||||
</Bevel>
|
||||
<small id="endpoint-constraint">Use an HTTPS endpoint reachable from the Workspace server.</small>
|
||||
<span id="endpoint-error" class="field-error">This endpoint uses HTTP. Enter an HTTPS URL.</span>
|
||||
</label>
|
||||
|
||||
<label class="form-field">
|
||||
<span>Architecture</span>
|
||||
<Bevel fill>
|
||||
<select name="architecture">
|
||||
<option>arm64</option>
|
||||
<option selected>x86_64</option>
|
||||
</select>
|
||||
</Bevel>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<BevelLine direction="x" length="100%" decorative />
|
||||
<div class="form-footer">
|
||||
<p class="unsaved"><span class="status__marker" data-tone="warning"></span>Unsaved changes</p>
|
||||
<Bevel>
|
||||
<button class="action action--primary" type="submit">Save runtime</button>
|
||||
</Bevel>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="showroom-section" aria-labelledby="operation-error-heading">
|
||||
<h2 id="operation-error-heading" class="showroom-heading">Operation error</h2>
|
||||
<BevelLine direction="x" length="100%" decorative />
|
||||
<div class="feedback feedback--error" role="alert">
|
||||
<div>
|
||||
<strong>Runtime was not saved.</strong>
|
||||
<p>The endpoint did not respond within 10 seconds. Check the address and try again.</p>
|
||||
</div>
|
||||
<Tooltip id="retry-runtime-help" text="Checks this endpoint again without saving other fields." placement="bottom" align="end">
|
||||
{#snippet children(descriptionId)}
|
||||
<Bevel>
|
||||
<button class="action action--secondary" type="button" aria-describedby={descriptionId}>Test endpoint</button>
|
||||
</Bevel>
|
||||
{/snippet}
|
||||
</Tooltip>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="showroom-section" aria-labelledby="permission-boundary-heading">
|
||||
<h2 id="permission-boundary-heading" class="showroom-heading">Permission boundary</h2>
|
||||
<BevelLine direction="x" length="100%" decorative />
|
||||
<div class="permission-read-view">
|
||||
<dl class="key-value">
|
||||
<div><dt>Credential source</dt><dd>Workspace secret store</dd></div>
|
||||
<BevelLine as="div" direction="x" length="100%" decorative />
|
||||
<div><dt>Access</dt><dd><span class="status"><span class="status__marker" data-tone="muted"></span>Read-only</span></dd></div>
|
||||
</dl>
|
||||
<p>Workspace owner permission is required to replace repository credentials.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="showroom-section" aria-labelledby="empty-heading">
|
||||
<h2 id="empty-heading" class="showroom-heading">Empty</h2>
|
||||
<BevelLine direction="x" length="100%" decorative />
|
||||
<div class="feedback">
|
||||
<strong>No Profile Sources.</strong>
|
||||
<Bevel>
|
||||
<button class="action action--secondary" type="button">Add profile source</button>
|
||||
</Bevel>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import { settingsBasePath, settingsNavigation } from './settings-navigation';
|
||||
|
||||
let {
|
||||
currentPath,
|
||||
content = null,
|
||||
}: {
|
||||
currentPath: string;
|
||||
content?: Snippet<[]> | null;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="settings-sidebar">
|
||||
{#if content}
|
||||
{@render content()}
|
||||
{:else}
|
||||
<nav class="sidebar-sections" aria-label="Settings sections">
|
||||
<div class="sidebar-nav-section">
|
||||
<div class="sidebar-list">
|
||||
{#each settingsNavigation as item, index}
|
||||
<a
|
||||
class="sidebar-link"
|
||||
class:active={index === 0 && currentPath === settingsBasePath}
|
||||
href={item.href}
|
||||
aria-current={index === 0 && currentPath === settingsBasePath ? 'page' : undefined}
|
||||
>
|
||||
<span class="sidebar-link-label">{item.label}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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); }
|
||||
|
||||
+2
-2
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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('<div class="border-matrix">') &&
|
||||
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("<Bevel pressed"),
|
||||
"Workspace showroom must demonstrate closed and adjacent plain borders",
|
||||
);
|
||||
assert(
|
||||
tactileGroupRule && !tactileGroupRule.includes("background"),
|
||||
"Showroom bordered items must leave their content area background unset",
|
||||
);
|
||||
assert(
|
||||
settingsSource.includes("<Bevel fill>") &&
|
||||
settingsSource.includes("<Bevel fill invalid>") &&
|
||||
settingsSource.includes('aria-invalid="true"') &&
|
||||
!settingsSource.includes("profile=") &&
|
||||
!settingsSource.includes("depth="),
|
||||
"Settings showroom must demonstrate plain and invalid borders",
|
||||
);
|
||||
});
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
@@ -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<URL[]> {
|
||||
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",
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user