refactor(web): split global CSS by ownership (#2609)

* refactor(web): split global CSS by ownership

* test(web): expand CSS imports in style checks

* fix(web): keep privacy consent banner above modals
This commit is contained in:
Marc Chan 2026-05-25 13:48:28 +08:00 committed by GitHub
parent 5d032aedec
commit 619087a6b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 27878 additions and 27821 deletions

View file

@ -143,6 +143,15 @@ root `pnpm tools-pr` script without a new explicit maintainer decision.
- `AskUserQuestionCard` (in `ToolCard.tsx`) prefers the live `onAnswerToolUse(toolUseId, content)` route (POSTs to `/api/runs/:id/tool-result`) and falls back to the legacy `onSubmitForm(text)` path when the run has already terminated. Selected chips persist across reloads by parsing the stored `tool_result.content` back into the selections shape.
- Tool group rendering uses `dedupeSnapshotToolRetries` to collapse identical `AskUserQuestion` retries (one card per unique input, keeping the latest tool_use_id) and `TodoWrite` snapshots (only the most recent call, since each call is a state replace).
## Web CSS ownership
- `apps/web/src/index.css` is an import-only cascade entrypoint. Do not add selectors or declarations there; add imports only when a truly global stylesheet is needed, and keep import order intentional.
- Shared global styles belong in `apps/web/src/styles/`: design tokens, base/reset rules, primitives, app-shell layout, and legacy cross-component selectors that cannot safely be scoped yet. Keep domain-level global files grouped by owner (for example `styles/viewer/` and `styles/workspace/`) instead of adding more large files directly under `styles/`.
- New component-owned UI styles should default to CSS Modules next to the component (`Component.module.css`) instead of expanding global stylesheets. This is preferred for isolated components, panels, menus, drawers, toolbars, cards, and form sections.
- When touching an existing component with nearby global styles, prefer migrating that component's local selectors to a CSS Module as part of the change if it is small and testable. Do not mix a large mechanical move with behavior/styling changes in the same patch.
- Keep global class names only for deliberate shared contracts: reusable primitives, theme hooks, third-party/content styling, cross-component layout, or selectors that rely on global cascade/specificity. Document any new global selector group with its owning feature.
- CSS refactors must preserve cascade semantics. For mechanical splits, verify expanded import content/order matches the previous stylesheet; for CSS Module migrations, validate the affected UI path with `pnpm --filter @open-design/web typecheck` and a focused build/test or visual check when practical.
## i18n keys
- `apps/web/src/i18n/types.ts` is the typed `Dict`; every key must be defined in all 18 locale files under `apps/web/src/i18n/locales/*.ts` (`ar`, `de`, `en`, `es-ES`, `fa`, `fr`, `hu`, `id`, `ja`, `ko`, `pl`, `pt-BR`, `ru`, `th`, `tr`, `uk`, `zh-CN`, `zh-TW`). Add the key to `types.ts` first; missing translations produce a typecheck error.

View file

@ -109,10 +109,13 @@ export function DesignSystemsSection({ cfg, setCfg }: Props) {
useEffect(() => {
if (!highlightedDesignSystemId) return;
const raf = window.requestAnimationFrame(() => {
cardRefs.current.get(highlightedDesignSystemId)?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
const card = cardRefs.current.get(highlightedDesignSystemId);
if (typeof card?.scrollIntoView === 'function') {
card.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
}
});
const timeout = window.setTimeout(() => {
setHighlightedDesignSystemId((current) =>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,23 @@
* { box-sizing: border-box; }
html, body, #root { height: 100%; margin: 0; }
body {
font-family: var(--sans);
color: var(--text);
background: var(--bg-app);
font-size: 13.5px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.od-loading-shell {
min-height: 100vh;
display: grid;
place-items: center;
color: var(--text-muted);
background: var(--bg-app);
font: 500 13px/1.4 var(--sans);
}

View file

@ -0,0 +1,772 @@
/* -------- Chat sticky header --------------------------------------- */
.chat-header {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 12px;
border-bottom: 1px solid var(--border);
background: var(--bg-panel);
position: sticky;
top: 0;
z-index: 4;
height: 44px;
}
.chat-header-tabs { display: inline-flex; gap: 16px; flex: 1; }
.chat-header-tab {
background: transparent;
border: none;
border-bottom: 2px solid transparent;
border-radius: 0;
padding: 8px 0;
font-size: 13px;
color: var(--text-muted);
font-weight: 500;
}
.chat-header-tab:hover { color: var(--text); background: transparent; border-color: transparent; }
.chat-header-tab.active {
color: var(--text);
border-bottom-color: var(--text);
}
.chat-active-conversation {
min-width: 0;
flex: 1;
display: inline-flex;
align-items: center;
gap: 6px;
}
.chat-active-conversation-title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
font-weight: 600;
color: var(--text);
}
.chat-active-conversation-rename {
width: 24px;
height: 24px;
padding: 0;
border: none;
background: transparent;
color: var(--text-faint);
border-radius: 5px;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
}
.chat-active-conversation-rename:hover {
background: var(--bg-subtle);
color: var(--text);
}
.chat-active-conversation-rename:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.chat-active-conversation-input {
width: 100%;
min-width: 0;
height: 28px;
padding: 3px 8px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg);
color: var(--text);
font-size: 13px;
font-weight: 600;
}
.chat-active-conversation-input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent-soft);
}
.chat-header-actions { display: inline-flex; gap: 4px; align-items: center; margin-left: auto; }
.chat-header-actions .icon-only {
width: 28px; height: 28px;
padding: 0;
background: transparent;
border: none;
border-radius: 6px;
color: var(--text-muted);
display: inline-flex;
align-items: center;
justify-content: center;
}
.chat-header-actions .icon-only:hover { background: var(--bg-subtle); color: var(--text); }
.chat-log {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 16px;
display: flex;
flex-direction: column;
gap: 14px;
min-width: 0;
}
/* -------- Messages -------------------------------------------------- */
.msg {
padding: 0;
background: transparent;
border: none;
white-space: normal;
word-wrap: break-word;
min-width: 0;
max-width: 100%;
}
.msg.user {
align-self: flex-end;
display: flex;
flex-direction: column;
align-items: flex-end;
max-width: min(78%, 560px);
}
.msg .role {
display: flex;
align-items: baseline;
gap: 8px;
font-size: 12.5px;
text-transform: none;
color: var(--text-strong);
margin-bottom: 4px;
letter-spacing: 0;
font-weight: 600;
}
.msg-time {
color: var(--text-faint);
font-size: 11px;
font-weight: 500;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.chat-day-separator {
display: flex;
align-items: center;
gap: 10px;
color: var(--text-faint);
font-size: 11px;
font-weight: 600;
margin: 4px 0 0;
}
.chat-day-separator::before,
.chat-day-separator::after {
content: '';
height: 1px;
background: var(--border);
flex: 1;
}
.msg.user .role::before { content: ''; }
.msg.user .role {
justify-content: flex-end;
color: var(--text-muted);
}
.msg.user .user-text {
white-space: pre-wrap;
color: #fff;
background: var(--selected);
border: 1px solid color-mix(in srgb, var(--selected) 88%, #000);
border-radius: 14px 14px 4px 14px;
padding: 8px 11px;
line-height: 1.45;
box-shadow: var(--shadow-xs);
}
.msg-plugin-chip {
display: inline-flex;
align-items: center;
gap: 8px;
margin: 4px 0 6px;
padding: 4px 10px 4px 8px;
border-radius: 999px;
background: var(--bg-subtle);
border: 1px solid var(--border);
font-size: 11.5px;
line-height: 1;
color: var(--text-muted);
max-width: 100%;
}
.msg-plugin-chip__dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent);
flex: 0 0 auto;
}
.msg-plugin-chip--design-system .msg-plugin-chip__dot {
background: #d76445;
}
.msg-plugin-chip__label {
display: inline-flex;
align-items: baseline;
gap: 6px;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.msg-plugin-chip__kind {
text-transform: uppercase;
letter-spacing: 0.04em;
font-weight: 600;
color: var(--text-faint);
}
.msg-plugin-chip__title {
color: var(--text);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.msg-plugin-chip__version,
.msg-plugin-chip__task {
color: var(--text-faint);
font-variant-numeric: tabular-nums;
}
.msg-plugin-chip__task {
border-left: 1px solid var(--border);
padding-left: 8px;
}
/* Wrapper for user bubble + copy button */
.msg.user .user-text-wrap {
position: relative;
display: inline-block;
max-width: 100%;
}
.msg.user .user-text-wrap .user-text {
padding-inline-end: 28px;
}
.msg.user .user-status-wrap {
display: flex;
justify-content: flex-end;
}
.user-status-card {
display: inline-flex;
align-items: center;
gap: 10px;
max-width: min(320px, 100%);
padding: 9px 11px;
border-radius: 14px 14px 4px 14px;
background: var(--bg-subtle);
border: 1px solid var(--border);
box-shadow: var(--shadow-xs);
color: var(--text);
text-align: start;
}
.user-status-card__icon {
width: 28px;
height: 28px;
border-radius: 8px;
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
color: var(--accent);
background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.user-status-card__copy {
min-width: 0;
display: grid;
gap: 2px;
}
.user-status-card__copy strong {
font-size: 12.5px;
line-height: 1.2;
font-weight: 650;
}
.user-status-card__copy span {
font-size: 11.5px;
line-height: 1.35;
color: var(--text-muted);
}
.msg.user .user-copy-btn {
position: absolute;
inset-block-start: 6px;
inset-inline-end: 6px;
padding: 4px;
border: none;
background: none;
border-radius: 4px;
opacity: 0;
transition: opacity 120ms ease;
color: rgba(255, 255, 255, 0.85);
cursor: pointer;
z-index: 1;
}
.msg.user .user-copy-btn:hover {
color: #fff;
background: rgba(255, 255, 255, 0.12);
}
.msg.user .user-text-wrap:hover .user-copy-btn,
.msg.user .user-text-wrap:focus-within .user-copy-btn,
.msg.user .user-copy-btn:focus-visible {
opacity: 1;
}
.msg.user .user-copy-btn:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 1px;
}
@media (hover: none) {
.msg.user .user-copy-btn {
opacity: 1;
}
}
.msg.assistant .prose { margin-top: 4px; }
.msg .artifact-badge {
display: inline-block;
margin-top: 8px;
padding: 2px 8px;
font-size: 11px;
background: var(--accent);
color: white;
border-radius: 4px;
}
.msg.error {
border: 1px solid var(--red-border);
background: var(--red-bg);
color: var(--red);
padding: 10px 12px;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.chat-error-text { min-width: 0; }
.chat-error-retry {
flex: 0 0 auto;
border-color: var(--red-border);
color: var(--red);
padding: 4px 10px;
}
/* -------- Composer -------------------------------------------------- */
.composer {
border-top: 1px solid var(--border);
padding: 10px;
display: flex;
flex-direction: column;
gap: 8px;
background: var(--bg-panel);
position: relative;
z-index: 1;
}
/* Elevation kicks in only when a pinned todo card is sitting directly
* above the composer. Without the card the chat scroll already provides
* its own visual edge; the shadow would just darken empty whitespace. */
.chat-pinned-todo + .composer {
box-shadow: 0 -10px 22px -10px rgba(0, 0, 0, 0.22);
}
/* Dark mode: the same alpha disappears against the dark panel, so push
* it harder and let it spread a bit further. */
[data-theme="dark"] .chat-pinned-todo + .composer {
box-shadow: 0 -14px 28px -12px rgba(0, 0, 0, 0.65);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .chat-pinned-todo + .composer {
box-shadow: 0 -14px 28px -12px rgba(0, 0, 0, 0.65);
}
}
.composer-shell {
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg-panel);
box-shadow: var(--shadow-xs);
padding: 8px 10px 6px;
display: flex;
flex-direction: column;
gap: 6px;
transition: border-color 120ms ease, box-shadow 120ms ease;
}
.composer-shell:focus-within {
border-color: var(--border-strong);
box-shadow: var(--shadow-sm);
}
.composer.drag-active .composer-shell {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.composer textarea {
min-height: 88px;
max-height: min(184px, 34vh);
border: none;
background: transparent;
padding: 4px 4px;
resize: none;
overflow-y: hidden;
}
.composer textarea:focus { outline: none; box-shadow: none; }
.composer-input-wrap {
position: relative;
display: flex;
flex-direction: column;
}
.composer-textarea-layer {
position: relative;
min-height: 88px;
}
.composer-input-overlay {
position: absolute;
inset: 0;
z-index: 2;
min-height: 88px;
padding: 0;
color: var(--text);
font: inherit;
line-height: normal;
pointer-events: none;
white-space: pre-wrap;
overflow: hidden;
overflow-wrap: anywhere;
}
.composer-input-overlay-inner {
min-height: 100%;
padding: 4px;
transform: translateY(calc(-1 * var(--composer-input-scroll, 0px)));
}
.composer-input-wrap.has-mention-overlay textarea {
color: transparent;
caret-color: var(--text);
}
.composer-input-wrap.has-mention-overlay textarea::selection {
background: color-mix(in srgb, var(--accent) 24%, transparent);
color: transparent;
}
.composer-inline-mention {
display: inline;
max-width: none;
margin: 0;
padding: 0;
border: 0;
border-radius: 5px;
background: color-mix(in srgb, var(--accent) 8%, transparent);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 12%, transparent);
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
color: color-mix(in srgb, var(--accent) 62%, var(--text));
font: inherit;
font-size: inherit;
font-weight: inherit;
line-height: inherit;
letter-spacing: inherit;
vertical-align: baseline;
overflow: visible;
text-overflow: clip;
white-space: inherit;
}
.composer-row {
display: flex;
align-items: center;
gap: 6px;
padding-top: 4px;
}
.composer-row .icon-btn {
width: 28px; height: 28px;
padding: 0;
background: transparent;
border: none;
border-radius: 6px;
color: var(--text-muted);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.composer-row .icon-btn:hover:not(:disabled) { background: var(--bg-subtle); color: var(--text); }
.composer-spacer { flex: 1; }
.composer-import {
background: transparent;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 4px 12px;
font-size: 12px;
color: var(--text-muted);
}
.composer-import:hover:not(:disabled) { background: var(--bg-subtle); color: var(--text); }
.composer-research {
display: inline-flex;
align-items: center;
gap: 6px;
background: transparent;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 4px 10px;
font-size: 12px;
color: var(--text-muted);
cursor: pointer;
}
.composer-research:hover:not(:disabled) { background: var(--bg-subtle); color: var(--text); }
.composer-research.on {
background: color-mix(in oklab, var(--accent) 12%, transparent);
border-color: var(--accent);
color: var(--accent);
}
.composer-research-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent);
}
.composer-send {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--accent);
border-color: var(--accent);
color: white;
font-weight: 500;
padding: 4px 14px;
font-size: 12.5px;
}
.composer-send:hover:not(:disabled) { background: var(--accent-hover); border-color: var(--accent-hover); }
.composer-send.stop { background: var(--text); border-color: var(--text); color: var(--bg); }
.composer-send.stop:hover { background: var(--text-strong); border-color: var(--text-strong); color: var(--bg); }
.composer-hint {
font-size: 11px;
color: var(--text-faint);
margin: 0 8px;
}
.preview-draw-note-input::placeholder {
color: rgba(255, 225, 210, 0.86);
}
.preview-draw-note-input:focus {
background: rgba(218, 97, 56, 0.26) !important;
border-color: rgba(255, 184, 140, 0.95) !important;
box-shadow: 0 0 0 3px rgba(218, 97, 56, 0.34), 0 0 18px rgba(218, 97, 56, 0.24) !important;
}
/* -------- Staged attachments -------------------------------------- */
.staged-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 4px 4px 0;
}
.staged-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 8px 4px 4px;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius);
max-width: 220px;
font-size: 11.5px;
box-shadow: var(--shadow-xs);
}
.staged-chip img {
width: 28px;
height: 28px;
border-radius: 6px;
object-fit: cover;
}
.staged-preview-trigger {
display: inline-flex;
align-items: center;
gap: 6px;
min-width: 0;
max-width: 100%;
flex: 1 1 auto;
padding: 0;
border: 0;
background: transparent;
color: inherit;
text-align: left;
}
.staged-preview-trigger:hover:not(:disabled) {
background: transparent;
border-color: transparent;
}
.staged-preview-trigger img {
flex: 0 0 28px;
width: 28px;
height: 28px;
}
.staged-preview-trigger .staged-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.staged-preview-modal {
position: fixed;
inset: 0;
z-index: 1200;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background: rgba(17, 24, 39, 0.44);
}
.staged-preview-card {
display: flex;
flex-direction: column;
gap: 10px;
width: min(720px, calc(100vw - 48px));
max-height: calc(100vh - 48px);
padding: 12px;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
}
.staged-preview-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-width: 0;
}
.staged-preview-head span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--text);
font-size: 13px;
font-weight: 600;
}
.staged-preview-card > img {
display: block;
max-width: 100%;
max-height: min(70vh, 640px);
object-fit: contain;
border-radius: var(--radius);
background: var(--bg);
}
.staged-comment {
border-radius: var(--radius-pill);
padding: 4px 6px 4px 10px;
}
.user-attachment.staged-comment {
max-width: 100%;
max-height: 200px;
overflow-y: auto;
}
.staged-comment .staged-name {
display: inline-flex;
align-items: center;
gap: 6px;
min-width: 0;
flex-wrap: wrap;
}
.staged-comment .staged-name strong {
color: var(--accent-strong);
font-weight: 650;
flex: 0 0 auto;
}
.staged-comment .staged-name span {
white-space: normal;
word-break: break-word;
}
.staged-comment button {
width: 20px;
height: 20px;
padding: 0;
border: 0;
border-radius: var(--radius-pill);
background: transparent;
color: var(--text-faint);
font-size: 14px;
line-height: 1;
}
.staged-comment button:hover {
background: var(--bg-subtle);
color: var(--text);
}
.staged-icon {
width: 28px; height: 28px;
border-radius: 6px;
background: var(--bg-subtle);
color: var(--text-muted);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.staged-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 140px;
color: var(--text);
}
.staged-remove {
background: transparent;
border: none;
padding: 0 2px;
color: var(--text-faint);
cursor: pointer;
font-size: 13px;
line-height: 1;
border-radius: 4px;
}
.staged-remove:hover { color: var(--red); background: var(--red-bg); }
.linked-dirs-row {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.linked-dir-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-size: 11.5px;
color: var(--text-muted);
max-width: 220px;
}
.linked-dir-chip svg { flex-shrink: 0; color: var(--text-muted); }
.linked-dir-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-attachments {
display: flex;
gap: 6px;
flex-wrap: wrap;
justify-content: flex-end;
margin-bottom: 8px;
}
.user-attachment {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 4px 10px 4px 4px;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: 11.5px;
max-width: 240px;
color: var(--text);
cursor: default;
text-align: left;
font: inherit;
font-size: 11.5px;
}
.user-attachment.openable { cursor: pointer; }
.user-attachment.openable:hover {
background: var(--bg-subtle);
border-color: var(--accent);
}
.user-attachment img {
width: 28px;
height: 28px;
object-fit: cover;
border-radius: 6px;
display: block;
}
.user-attachment .staged-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View file

@ -0,0 +1,284 @@
/* -------- Buttons --------------------------------------------------- */
button {
font: inherit;
color: var(--text);
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 6px 12px;
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease, color 120ms ease, box-shadow 120ms ease;
}
button:hover:not(:disabled) { background: var(--bg-subtle); border-color: var(--border-strong); }
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
button.primary {
background: var(--accent);
border-color: var(--accent);
color: white;
font-weight: 500;
box-shadow: 0 1px 0 rgba(180, 90, 59, 0.18) inset, var(--shadow-xs);
}
button.primary:hover:not(:disabled) {
background: var(--accent-hover);
border-color: var(--accent-hover);
}
button.primary-ghost {
background: var(--bg-panel);
border-color: var(--accent);
color: var(--accent);
font-weight: 500;
}
button.primary-ghost:hover:not(:disabled) { background: var(--accent-tint); }
button.ghost {
background: transparent;
border-color: var(--border);
color: var(--text);
}
button.ghost:hover:not(:disabled) { background: var(--bg-subtle); border-color: var(--border-strong); }
/*
* Transient success state for ghost buttons. Used by the Media
* Providers "Reload from daemon" button after a successful reload
* the button briefly turns green with a check icon for ~2s then snaps
* back to its idle treatment, replacing what used to be a permanent
* success paragraph under the section header. Reusable on any
* ghost button that wants the same "did it" pulse.
*/
button.ghost.is-success-flash {
border-color: var(--green-border, color-mix(in srgb, #1f9d55 32%, var(--border)));
background: var(--green-bg, color-mix(in srgb, #1f9d55 8%, transparent));
color: var(--green, #137a3d);
}
button.ghost.is-success-flash:hover:not(:disabled) {
background: var(--green-bg, color-mix(in srgb, #1f9d55 14%, transparent));
}
button.ghost.is-success-flash svg {
color: currentColor;
}
/*
* Visually-hidden but assistive-tech accessible. We use it to keep
* a `role="status"` live-region for actions whose visible feedback
* is too transient (e.g. a 2s button-label flash) for a screen
* reader to catch reliably. Borrowed from Tailwind's `sr-only` so
* it reads as the same primitive everyone already knows.
*/
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
button.subtle {
background: var(--bg-subtle);
border-color: transparent;
color: var(--text);
}
button.subtle:hover:not(:disabled) { background: var(--bg-muted); }
button.icon-btn { padding: 6px 10px; font-size: 13px; }
button:disabled { opacity: 0.5; cursor: not-allowed; }
/* -------- Inputs ---------------------------------------------------- */
input, textarea, select {
font: inherit;
color: var(--text);
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 7px 10px;
width: 100%;
transition: border-color 120ms ease, box-shadow 120ms ease;
}
input::placeholder, textarea::placeholder { color: var(--text-faint); }
input:focus, textarea:focus, select:focus {
outline: none;
border-color: var(--selected);
box-shadow: 0 0 0 3px var(--selected-soft);
}
/* Entry sidebar form fields keep a quieter, neutral focus so the orange
"Create" CTA stays the loudest thing in the panel. A low-opacity black
halo reads as "focused" without re-introducing the global blue/accent
ring. Using rgba directly because the Next CSS pipeline collapses
`color-mix(in srgb, var(--text) 8%, transparent)` to a solid var(--text)
ring (would render a 3px black band too loud). */
.entry-side input:focus,
.entry-side textarea:focus,
.entry-side select:focus {
border-color: var(--text);
box-shadow: 0 0 0 3px rgba(28, 27, 26, 0.08);
}
/* Native <select> on macOS uses its own intrinsic min-height that ends up
shorter than <input> at the same `padding: 7px 10px` rule above, so any
form that flex-aligns an input next to a select (e.g. the memory editor's
Title + Type row) renders with mismatched heights. Stripping the native
chrome lets the shared padding and inherited line-height compute the
same box dimensions as the input, then we paint our own chevron via a
background SVG. The chevron color is a per-theme override so it stays
readable against the panel in both light and dark. */
select {
padding-right: 32px;
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%2374716b' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 5 6 8 9 5'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
background-size: 12px 12px;
}
select::-ms-expand { display: none; }
[data-theme='dark'] select {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%239a9690' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 5 6 8 9 5'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
background-size: 12px 12px;
}
@media (prefers-color-scheme: dark) {
html:not([data-theme]) select {
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%239a9690' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='3 5 6 8 9 5'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
background-size: 12px 12px;
}
}
textarea { resize: vertical; font-family: inherit; }
.od-select {
position: relative;
width: 100%;
min-width: 0;
}
.od-select-trigger {
width: 100%;
min-width: 0;
min-height: 36px;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
padding: 7px 10px;
font: inherit;
color: var(--text);
text-align: left;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
cursor: pointer;
transition: border-color 120ms ease, box-shadow 120ms ease, background 120ms ease;
}
.od-select-trigger:hover:not(:disabled),
.od-select-trigger[aria-expanded='true'] {
border-color: var(--border-strong);
background: var(--bg-subtle);
}
.od-select-trigger:focus-visible,
.od-select-trigger[aria-expanded='true'] {
outline: none;
border-color: var(--selected);
box-shadow: 0 0 0 3px var(--selected-soft);
}
.od-select-trigger:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.od-select-trigger svg {
color: var(--text-muted);
transition: transform 120ms ease;
}
.od-select-trigger[aria-expanded='true'] svg {
transform: rotate(180deg);
}
.od-select-value,
.od-select-option-label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.od-select-menu {
z-index: 9000;
padding: 4px;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
max-width: calc(100vw - 24px);
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-lg);
}
.od-select-menu.portal {
position: fixed;
}
.od-select-menu.inline {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
max-height: min(280px, 48vh);
}
.od-select-option {
width: 100%;
min-width: 0;
min-height: 30px;
display: grid;
grid-template-columns: minmax(0, 1fr) 16px;
align-items: center;
gap: 8px;
padding: 6px 8px;
color: var(--text);
background: transparent;
border: 0;
border-radius: 6px;
font: inherit;
font-size: 12.5px;
text-align: left;
cursor: pointer;
}
.od-select-option:hover:not(:disabled),
.od-select-option.active:not(:disabled) {
background: var(--bg-subtle);
}
.od-select-option.selected {
color: var(--text);
font-weight: 600;
}
.od-select-option:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.od-select-option-check {
display: inline-flex;
justify-content: center;
color: var(--selected);
opacity: 0;
}
.od-select-option.selected .od-select-option-check {
opacity: 1;
}
.od-select-group + .od-select-group {
margin-top: 4px;
padding-top: 4px;
border-top: 1px solid var(--border-soft);
}
.od-select-group-label {
padding: 6px 8px 4px;
color: var(--text-faint);
font-size: 11px;
font-weight: 600;
}
code {
font-family: var(--mono);
background: var(--bg-subtle);
padding: 1px 5px;
border-radius: 4px;
font-size: 0.92em;
color: var(--text);
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,164 @@
/* ============================================================
Open Design neutral product workspace
============================================================ */
:root {
/* Surface palette neutral app chrome that does not bias generated artifacts.
Keep warm/brand colors out of preview backgrounds; generated product UI
must choose its own palette through the active skill/design brief. */
--bg: #faf9f7;
--bg-app: #faf9f7;
--bg-panel: #ffffff;
--bg-subtle: #eef1f5;
--bg-muted: #e4e8ef;
--bg-elevated: #ffffff;
--border: #e1e5eb;
--border-strong: #c9d0da;
--border-soft: #edf0f4;
--text: #1a1916;
--text-strong: #0d0c0a;
--text-muted: #74716b;
--text-soft: #989590;
--text-faint: #b3b0a8;
/* Accent — Open Design action color for app chrome only. */
--accent: #c96442;
--accent-strong: #b45a3b;
--accent-soft: #f5d8cb;
--accent-tint: #fbeee5;
--accent-hover: #b45a3b;
/* Semantic accent tints used by tool / status pills. */
--green: #1f7a3a;
--green-bg: #e8f7ee;
--green-border: #c6ead2;
--blue: #2348b8;
--blue-bg: #e8efff;
--blue-border: #c8d6ff;
--purple: #6c3aa6;
--purple-bg: #f3ecf9;
--purple-border: #e4d4f1;
--red: #9c2a25;
--red-bg: #fdecea;
--red-border: #f5c6c2;
--amber: #b26200;
--amber-bg: #fff3e0;
--shadow-xs: 0 1px 0 rgba(28, 27, 26, 0.04);
--shadow-sm: 0 1px 2px rgba(28, 27, 26, 0.05), 0 1px 3px rgba(28, 27, 26, 0.04);
--shadow-md: 0 6px 24px rgba(28, 27, 26, 0.07), 0 2px 6px rgba(28, 27, 26, 0.04);
--shadow-lg: 0 24px 60px rgba(28, 27, 26, 0.16), 0 8px 16px rgba(28, 27, 26, 0.07);
--radius-sm: 8px;
--radius: 12px;
--radius-lg: 16px;
--radius-pill: 999px;
/* "Active option" indicator color, intentionally separate from --accent
(brand orange used by primary CTAs like Create / Save). Use --selected
for "this option is the one currently picked" affordances selected
fidelity card, focused input ring, active filter pill so a primary
CTA and a selected state can coexist on the same screen without
competing. --selected-soft is the 16% tint for outer rings / fills. */
--selected: #2563eb;
--selected-soft: rgba(37, 99, 235, 0.16);
--serif: 'Source Serif Pro', 'Source Serif 4', 'Iowan Old Style', 'Apple Garamond', Georgia, 'Times New Roman', serif;
--sans: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--mono: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace;
}
/* Dark theme variables shared between explicit [data-theme="dark"] and the
OS-level prefers-color-scheme media query (system mode = no data-theme attr). */
[data-theme="dark"] {
color-scheme: dark;
--bg: #1a1917;
--bg-app: #1a1917;
--bg-panel: #222120;
--bg-subtle: #272523;
--bg-muted: #2e2c29;
--bg-elevated: #2a2825;
--border: #333128;
--border-strong: #46433c;
--border-soft: #2a2825;
--text: #e8e4dc;
--text-strong: #f2ede4;
--text-muted: #9a9690;
--text-soft: #6e6b65;
--text-faint: #4e4b46;
--accent: #d97a56;
--accent-strong: #e8896a;
--accent-soft: #3d2318;
--accent-tint: #2e1a12;
--accent-hover: #e8896a;
--green: #4caf72;
--green-bg: #0f2a18;
--green-border: #1a4028;
--blue: #6b8fe8;
--blue-bg: #0f1a38;
--blue-border: #1a2c58;
--purple: #a87dd4;
--purple-bg: #1e1030;
--purple-border: #321a50;
--red: #e06b65;
--red-bg: #2a0e0c;
--red-border: #451714;
--amber: #e09a40;
--amber-bg: #2a1a04;
--shadow-xs: 0 1px 0 rgba(0, 0, 0, 0.2);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 3px rgba(0, 0, 0, 0.2);
--shadow-md: 0 6px 24px rgba(0, 0, 0, 0.4), 0 2px 6px rgba(0, 0, 0, 0.25);
--shadow-lg: 0 24px 60px rgba(0, 0, 0, 0.6), 0 8px 16px rgba(0, 0, 0, 0.3);
}
/* System mode: follow OS preference when no explicit data-theme is set. */
@media (prefers-color-scheme: dark) {
html:not([data-theme]) {
color-scheme: dark;
--bg: #1a1917;
--bg-app: #1a1917;
--bg-panel: #222120;
--bg-subtle: #272523;
--bg-muted: #2e2c29;
--bg-elevated: #2a2825;
--border: #333128;
--border-strong: #46433c;
--border-soft: #2a2825;
--text: #e8e4dc;
--text-strong: #f2ede4;
--text-muted: #9a9690;
--text-soft: #6e6b65;
--text-faint: #4e4b46;
--accent: #d97a56;
--accent-strong: #e8896a;
--accent-soft: #3d2318;
--accent-tint: #2e1a12;
--accent-hover: #e8896a;
--green: #4caf72;
--green-bg: #0f2a18;
--green-border: #1a4028;
--blue: #6b8fe8;
--blue-bg: #0f1a38;
--blue-border: #1a2c58;
--purple: #a87dd4;
--purple-bg: #1e1030;
--purple-border: #321a50;
--red: #e06b65;
--red-bg: #2a0e0c;
--red-border: #451714;
--amber: #e09a40;
--amber-bg: #2a1a04;
--shadow-xs: 0 1px 0 rgba(0, 0, 0, 0.2);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 3px rgba(0, 0, 0, 0.2);
--shadow-md: 0 6px 24px rgba(0, 0, 0, 0.4), 0 2px 6px rgba(0, 0, 0, 0.25);
--shadow-lg: 0 24px 60px rgba(0, 0, 0, 0.6), 0 8px 16px rgba(0, 0, 0, 0.3);
}
}

View file

@ -0,0 +1,767 @@
/* Code viewer with line numbers */
.code-viewer {
background: var(--bg-panel);
margin: 0;
padding: 0;
display: grid;
grid-template-columns: auto 1fr;
font-family: var(--mono);
font-size: 12px;
line-height: 1.6;
min-height: 100%;
}
.code-viewer .gutter {
background: var(--bg);
color: var(--text-faint);
text-align: right;
padding: 16px 12px 16px 16px;
user-select: none;
border-right: 1px solid var(--border-soft);
white-space: pre;
font-variant-numeric: tabular-nums;
}
.code-viewer .lines {
padding: 16px 16px 16px 18px;
white-space: pre;
overflow-x: auto;
color: var(--text);
}
.viewer-empty {
padding: 48px 24px;
text-align: center;
color: var(--text-muted);
font-size: 13px;
}
/* Compact horizontal variant for the upload-failure banner: the message
sits inline next to a dismiss button so the user can clear the stale
notice without changing tabs. Issue #786. */
.viewer-empty.viewer-empty-dismissible {
padding: 12px 16px;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
text-align: left;
}
.document-preview {
max-width: 860px;
margin: 0 auto;
padding: 32px 40px 56px;
color: var(--text);
}
.document-preview h2 {
margin: 0 0 24px;
font-size: 20px;
line-height: 1.25;
}
.document-preview section {
border-top: 1px solid var(--border-soft);
padding-top: 18px;
margin-top: 18px;
}
.document-preview h3 {
margin: 0 0 12px;
font-size: 13px;
color: var(--text-muted);
font-weight: 600;
}
.document-preview p {
margin: 0 0 8px;
font-size: 14px;
line-height: 1.65;
white-space: pre-wrap;
}
.markdown-rendered {
max-width: 900px;
margin: 0 auto;
padding: 24px 28px 40px;
color: var(--text);
line-height: 1.65;
white-space: normal;
}
.markdown-status {
margin: 12px auto 0;
max-width: 900px;
padding: 8px 10px;
border: 1px solid var(--border-soft);
border-radius: 8px;
background: var(--bg-panel);
color: var(--text-muted);
font-size: 12px;
}
.markdown-status-error {
border-color: color-mix(in oklab, var(--danger, #d04b4b) 45%, var(--border-soft));
color: var(--danger, #d04b4b);
}
.markdown-rendered h1,
.markdown-rendered h2,
.markdown-rendered h3,
.markdown-rendered h4,
.markdown-rendered h5,
.markdown-rendered h6 {
margin: 20px 0 10px;
line-height: 1.25;
}
.markdown-rendered p { margin: 10px 0; }
.markdown-rendered ul,
.markdown-rendered ol {
margin: 10px 0;
padding-left: 24px;
}
.markdown-rendered blockquote {
margin: 12px 0;
padding: 8px 12px;
border-left: 3px solid var(--border);
color: var(--text-muted);
background: var(--bg-panel);
}
.markdown-code-block {
position: relative;
}
.markdown-code-copy {
position: absolute;
top: 18px;
right: 18px;
z-index: 1;
border: 1px solid var(--border-soft);
border-radius: 999px;
background: color-mix(in oklab, var(--bg-panel) 92%, black 8%);
color: var(--text-muted);
font: inherit;
font-size: 11px;
font-weight: 600;
line-height: 1;
padding: 7px 10px;
cursor: pointer;
opacity: 0;
transform: translateY(-2px);
transition:
opacity 120ms ease,
transform 120ms ease,
color 120ms ease,
border-color 120ms ease,
background 120ms ease;
}
.markdown-code-block:hover .markdown-code-copy,
.markdown-code-block:focus-within .markdown-code-copy {
opacity: 1;
transform: translateY(0);
}
@media (hover: none) {
.markdown-code-copy {
opacity: 1;
transform: translateY(0);
}
}
.markdown-code-copy:hover,
.markdown-code-copy:focus-visible {
color: var(--text);
border-color: var(--border);
background: var(--bg-elevated, var(--bg));
}
.markdown-code-toast {
position: absolute;
top: 18px;
right: 82px;
z-index: 1;
border-radius: 999px;
background: color-mix(in oklab, var(--accent) 18%, var(--bg-panel));
color: var(--text);
font-size: 11px;
font-weight: 600;
line-height: 1;
padding: 7px 10px;
box-shadow: 0 10px 26px color-mix(in oklab, var(--accent) 18%, transparent);
}
.markdown-rendered pre {
margin: 12px 0;
background: var(--bg-panel);
border: 1px solid var(--border-soft);
border-radius: 8px;
padding: 40px 12px 12px;
overflow: auto;
}
.markdown-rendered code {
font-family: var(--mono);
font-size: 12px;
}
.markdown-rendered a { color: var(--accent); }
.image-body {
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.image-body img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
background: white;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
.sketch-preview {
width: 100%;
height: 100%;
min-height: 0;
}
.sketch-preview svg {
width: 100%;
height: 100%;
display: block;
background: white;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
.sketch-preview.loading {
background:
radial-gradient(circle at 8px 8px, #d7d4ce 1px, transparent 1px),
#f7f5f1;
background-size: 16px 16px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
.sketch-preview-empty-mark path {
stroke: #bfbcb6;
stroke-width: 6;
stroke-linecap: round;
fill: none;
}
/* Sketch editor */
.sketch-editor {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
background: var(--bg);
}
.sketch-toolbar {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 12px;
border-bottom: 1px solid var(--border);
background: var(--bg-panel);
flex-wrap: wrap;
}
.sketch-tool {
padding: 6px 10px;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: 6px;
font-size: 14px;
line-height: 1;
min-width: 32px;
}
.sketch-tool:hover { background: var(--bg-subtle); }
.sketch-tool.active { background: var(--accent); color: white; border-color: var(--accent); }
.sketch-divider { width: 1px; height: 20px; background: var(--border); margin: 0 4px; }
.sketch-color { width: 32px; height: 28px; padding: 0; border: 1px solid var(--border); border-radius: 6px; cursor: pointer; }
.sketch-size { width: 80px; background: transparent; border: none; }
.sketch-spacer { flex: 1; }
.sketch-canvas-wrap { flex: 1; min-height: 0; position: relative; background: var(--bg); }
.sketch-canvas-wrap canvas { display: block; cursor: crosshair; }
/* ===========================================================
Streaming chat: assistant message header, prose, thinking,
tool cards, status pills, grouped action card.
=========================================================== */
.chat-empty-wrap {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 18px;
padding: 24px 8px;
min-height: 100%;
}
.chat-empty {
color: var(--text-muted);
font-size: 13px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 6px;
max-width: 44ch;
}
.chat-empty-title { font-weight: 600; color: var(--text-strong); font-size: 15px; }
.chat-empty-hint { line-height: 1.6; }
.chat-examples {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
max-width: 520px;
}
.chat-example {
position: relative;
display: flex;
align-items: center;
gap: 12px;
width: 100%;
padding: 14px 16px;
text-align: left;
cursor: pointer;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow-xs);
color: var(--text);
font: inherit;
overflow: hidden;
transition:
transform 160ms ease,
border-color 160ms ease,
box-shadow 160ms ease,
background-color 160ms ease;
opacity: 0;
animation: chat-example-in 380ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
.chat-example::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
135deg,
var(--accent-tint) 0%,
transparent 55%
);
opacity: 0;
transition: opacity 200ms ease;
pointer-events: none;
}
.chat-example:hover {
transform: translateY(-2px);
border-color: var(--accent);
box-shadow: var(--shadow-md);
background: var(--bg-panel);
}
.chat-example:hover::before {
opacity: 1;
}
.chat-example:active {
transform: translateY(0);
}
.chat-example:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.chat-example-icon {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
flex-shrink: 0;
border-radius: var(--radius-sm);
background: var(--accent);
color: #fff;
font-size: 18px;
line-height: 1;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.04), inset 0 -1px 0 rgba(0, 0, 0, 0.08);
}
.chat-example-body {
position: relative;
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
flex: 1;
}
.chat-example-head {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.chat-example-title {
font-weight: 600;
color: var(--text-strong);
font-size: 13.5px;
}
.chat-example-tag {
font-family: var(--mono);
font-size: 10.5px;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--accent);
background: var(--accent-tint);
border: 1px solid var(--accent-soft);
padding: 1px 7px;
border-radius: var(--radius-pill);
line-height: 1.5;
white-space: nowrap;
}
.chat-example-prompt {
color: var(--text-muted);
font-size: 12.5px;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.chat-example-cta {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
flex-shrink: 0;
border-radius: 50%;
background: var(--bg-subtle);
color: var(--text-muted);
font-size: 13px;
transition: background-color 160ms ease, color 160ms ease, transform 160ms ease;
}
.chat-example:hover .chat-example-cta {
background: var(--accent);
color: #fff;
transform: translateX(2px);
}
/* Connect-your-repo CTA in the empty chat state. Shares the example cards'
sizing and accent system but stands apart with a tinted fill so it reads as
guidance rather than another starter prompt. */
.chat-connect-repo {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
max-width: 520px;
padding: 14px 16px;
background: var(--accent-tint);
border: 1px solid var(--accent-soft);
border-radius: var(--radius);
box-shadow: var(--shadow-xs);
opacity: 0;
animation: chat-example-in 380ms cubic-bezier(0.22, 1, 0.36, 1) 220ms forwards;
}
.chat-connect-repo-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
flex-shrink: 0;
border-radius: var(--radius-sm);
background: var(--accent);
color: #fff;
}
.chat-connect-repo-body {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
flex: 1;
}
.chat-connect-repo-title {
font-weight: 600;
color: var(--text-strong);
font-size: 13.5px;
}
.chat-connect-repo-text {
color: var(--text-muted);
font-size: 12.5px;
line-height: 1.5;
}
.chat-connect-repo button {
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
@keyframes chat-example-in {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.assistant-header {
display: flex;
align-items: center;
gap: 8px;
font-size: 11.5px;
color: var(--text-muted);
margin-bottom: 8px;
}
.assistant-header .dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--text-muted);
}
.assistant-header .dot[data-active="true"] {
background: var(--accent);
animation: pulse 1.2s ease-in-out infinite;
}
.assistant-label { font-weight: 600; color: var(--text-strong); font-size: 12.5px; }
.assistant-stats { font-variant-numeric: tabular-nums; margin-left: auto; }
.assistant-flow {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 0;
}
.prose-block { line-height: 1.6; color: var(--text); }
.prose-block .md-p { margin: 0; }
.prose-block .md-p + .md-p { margin-top: 8px; }
.prose-block .md-h { margin: 10px 0 4px; line-height: 1.3; font-weight: 600; }
.prose-block .md-h1 { font-size: 18px; }
.prose-block .md-h2 { font-size: 16px; }
.prose-block .md-h3 { font-size: 14px; }
.prose-block .md-h4 { font-size: 13px; }
.prose-block .md-ul, .prose-block .md-ol { margin: 4px 0; padding-left: 20px; }
.prose-block .md-ul li, .prose-block .md-ol li { margin: 2px 0; }
.prose-block .md-inline-code {
background: var(--bg-subtle);
border: 1px solid var(--border);
border-radius: 4px;
padding: 1px 5px;
font-family: var(--mono);
font-size: 0.92em;
}
.prose-block .md-code {
background: var(--bg-subtle);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 10px 12px;
margin: 6px 0;
overflow-x: auto;
font-size: 12px;
line-height: 1.5;
}
.prose-block .md-code code { font-family: var(--mono); }
.prose-block .md-link {
color: var(--accent);
text-decoration: underline;
word-break: break-word;
overflow-wrap: anywhere;
}
.prose-block .md-link-bare {
/* Long bare URLs (OAuth flows etc.) need to wrap mid-string so they
don't escape the chat column. break-word is enough for most agents,
but `anywhere` covers query strings with no spaces. */
word-break: break-all;
overflow-wrap: anywhere;
}
.prose-block .md-hr {
border: none;
border-top: 1px solid var(--border);
margin: 10px 0;
}
/* GFM pipe tables. Rules are intentionally unscoped so both the chat path
(`.prose-block`) and the artifact preview (`.markdown-rendered`) get
the same treatment. Horizontal scroll lives on the wrapper so the
inner <table> can keep `display: table; width: 100%` and distribute
column widths across the container instead of collapsing to content. */
.md-table-wrap {
display: block;
overflow-x: auto;
max-width: 100%;
margin: 12px 0;
border: 1px solid var(--border);
border-radius: 6px;
}
.md-table {
width: 100%;
border-collapse: collapse;
font-size: 0.95em;
}
.md-table th,
.md-table td {
padding: 8px 12px;
text-align: left;
vertical-align: top;
border-bottom: 1px solid var(--border);
}
.md-table th + th,
.md-table td + td {
border-left: 1px solid var(--border);
}
.md-table thead th {
background: var(--bg-panel);
color: var(--text);
font-weight: 600;
border-bottom-color: var(--border);
}
.md-table tbody tr:last-child td {
border-bottom: none;
}
.md-table tbody tr:nth-child(even) td {
background: color-mix(in oklab, var(--bg-panel) 40%, transparent);
}
.op-waiting {
font-size: 12px;
color: var(--text-muted);
font-style: italic;
}
/* Thinking blocks */
.thinking-block {
background: rgba(108, 58, 166, 0.04);
border: 1px solid rgba(108, 58, 166, 0.16);
border-radius: var(--radius-sm);
}
.thinking-toggle {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 7px 12px;
background: transparent;
border: none;
text-align: left;
font-size: 12px;
color: var(--text);
border-radius: var(--radius-sm);
}
.thinking-toggle:hover { background: rgba(108, 58, 166, 0.05); border-color: transparent; }
.thinking-icon { color: var(--purple); }
.thinking-label { font-weight: 500; }
.thinking-preview {
flex: 1;
color: var(--text-muted);
font-style: italic;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.thinking-chev { color: var(--text-muted); font-size: 10px; }
.thinking-body {
margin: 0;
padding: 0 14px 12px 14px;
font-family: var(--mono);
font-size: 11px;
line-height: 1.55;
color: var(--text-muted);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
/* Status pills */
.status-pill {
display: inline-flex;
align-self: flex-start;
align-items: flex-start;
flex-wrap: wrap;
gap: 6px;
max-width: 100%;
padding: 3px 12px;
background: var(--bg-subtle);
border: 1px solid var(--border);
border-radius: var(--radius-pill);
font-size: 11.5px;
color: var(--text-muted);
}
.status-label { letter-spacing: 0.02em; }
.status-detail {
min-width: 0;
max-width: 100%;
color: var(--text);
overflow-wrap: anywhere;
white-space: normal;
}
/* Grouped tool / action card — the collapsible pill from screenshot 9 */
.action-card {
display: flex;
flex-direction: column;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg-panel);
overflow: hidden;
}
.action-card-toggle {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 8px 12px;
background: transparent;
border: none;
text-align: left;
font-size: 12.5px;
color: var(--text);
cursor: pointer;
border-radius: var(--radius);
}
.action-card-toggle:hover { background: var(--bg-subtle); border-color: transparent; }
.action-card-toggle .ico {
width: 20px; height: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 4px;
background: var(--bg-subtle);
color: var(--text-muted);
font-size: 12px;
font-family: var(--mono);
flex-shrink: 0;
}
.action-card-toggle .summary { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.action-card-toggle .summary strong { font-weight: 500; }
.action-card-toggle .chev { color: var(--text-faint); font-size: 10px; flex-shrink: 0; }
.action-card-toggle.running .ico { animation: pulse 1.6s ease-in-out infinite; background: var(--purple-bg); color: var(--purple); }
.action-card-body {
padding: 0 12px 10px;
display: flex;
flex-direction: column;
gap: 6px;
border-top: 1px solid var(--border-soft);
}
.action-card-body > .op-card { border-color: transparent; box-shadow: none; padding: 4px 0; }
.action-card-body > .op-card .op-card-head { padding: 6px 0; }
/* Tool / operation cards — single, ungrouped */
.op-card {
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg-panel);
overflow: hidden;
box-shadow: var(--shadow-xs);
min-width: 0;
max-width: 100%;
}
.op-card-head {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
font-size: 12.5px;
flex-wrap: wrap;
}
.op-icon {
width: 22px;
height: 22px;
border-radius: 6px;
background: var(--bg-subtle);
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
font-size: 12px;
font-family: var(--mono);
flex-shrink: 0;
}
.op-icon-write { background: var(--green-bg); color: var(--green); }
.op-icon-edit { background: var(--amber-bg); color: var(--amber); }
.op-icon-read { background: var(--blue-bg); color: var(--blue); }
.op-title { font-weight: 500; }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,334 @@
/* ============================================================
Note: the entry-view home redesign (left rail, centered hero,
recent projects strip, plugins-home section, new-project modal)
lives in `src/styles/home/`. Imported from `app/layout.tsx`
alongside this file so upstream `index.css` rebases stay narrow.
============================================================ */
.settings-skills .skills-row-summary-btn:hover .skills-row-chevron {
color: var(--text-muted);
}
.settings-skills .skills-row-expanded .skills-row-chevron {
transform: rotate(180deg);
color: var(--text-muted);
}
.settings-skills .skills-row-actions {
display: inline-flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.settings-skills .skills-row-actions .icon-btn {
width: 26px;
height: 26px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
background: transparent;
border: 1px solid transparent;
border-radius: 6px;
color: var(--text-faint);
cursor: pointer;
transition: background-color 120ms ease, border-color 120ms ease,
color 120ms ease;
}
.settings-skills .skills-row-actions .icon-btn:hover {
background: var(--bg-panel);
border-color: var(--border);
color: var(--text);
}
.settings-skills .skills-row-enable {
margin-left: 4px;
}
.settings-skills .skills-delete-confirm {
display: inline-flex;
gap: 6px;
}
.settings-skills .skills-delete-confirm .btn {
padding: 4px 10px;
font-size: 12px;
border-radius: 6px;
}
.settings-skills .skills-row-detail {
display: flex;
flex-direction: column;
gap: 14px;
padding: 4px 14px 14px;
border-top: 1px dashed var(--border);
margin: 0 4px;
}
.settings-skills .skills-row-section {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
}
.settings-skills .skills-row-section h5 {
margin: 0;
font-size: 10.5px;
letter-spacing: 0.07em;
text-transform: uppercase;
color: var(--text-faint);
font-weight: 600;
}
.settings-skills .skills-row-section .library-preview-body {
margin: 0;
max-height: 320px;
overflow: auto;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 12px;
font-size: 12.5px;
line-height: 1.55;
}
.settings-skills .skills-file-tree {
list-style: none;
margin: 0;
padding: 0;
font-size: 12px;
color: var(--text-muted);
overflow-y: auto;
max-height: 240px;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 10px;
}
.settings-skills .skills-file-entry {
display: flex;
align-items: center;
gap: 6px;
padding: 3px 0;
}
.settings-skills .skills-file-entry-directory {
color: var(--text);
font-weight: 500;
}
.settings-skills .skills-file-size {
margin-left: auto;
color: var(--text-faint);
font-variant-numeric: tabular-nums;
font-size: 11px;
}
.settings-skills > .skills-draft {
margin: 0;
}
.settings-skills .skills-row > .skills-draft {
border: none;
border-top: 1px dashed var(--border);
border-radius: 0 0 10px 10px;
margin: 0;
background: transparent;
}
.settings-skills .skills-draft .skills-draft-head {
display: flex;
align-items: baseline;
gap: 8px;
margin-bottom: 4px;
}
.settings-skills .skills-draft .skills-draft-head h4 {
margin: 0;
font-size: 13px;
font-weight: 600;
}
.settings-skills .skills-draft .skills-draft-sub {
margin: 0;
font-size: 11px;
color: var(--text-faint);
font-family: var(--mono);
}
.mcp-json-helper {
margin-top: 10px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg-subtle);
overflow: hidden;
}
.mcp-json-helper-toggle {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
border: none;
border-radius: calc(var(--radius) - 2px);
background: transparent;
font-size: 12px;
font-weight: 500;
line-height: 1.5;
color: var(--text-muted);
cursor: pointer;
transition:
color 120ms ease,
background 120ms ease,
box-shadow 120ms ease;
}
.mcp-json-helper-toggle:hover {
color: var(--text);
background: transparent;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-soft) 30%, transparent);
}
.mcp-json-helper.is-open .mcp-json-helper-toggle {
border: none;
border-radius: 0;
background: transparent;
box-shadow: inset 0 -1px 0 var(--border);
}
.mcp-json-helper.is-open .mcp-json-helper-toggle:hover {
box-shadow: inset 0 -1px 0 var(--border);
}
.mcp-json-helper-toggle span:first-child {
text-align: left;
}
.mcp-json-helper-toggle-icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--text-soft);
}
.mcp-json-helper-example {
display: flex;
flex-direction: column;
}
.mcp-json-helper-example-head {
padding: 10px 12px 0;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--text-soft);
}
.mcp-json-helper-code {
margin: 0 12px 12px;
padding: 14px;
border: 1px solid var(--border);
border-radius: calc(var(--radius) - 4px);
background:
linear-gradient(
180deg,
color-mix(in srgb, var(--bg) 92%, black) 0%,
color-mix(in srgb, var(--bg) 86%, black) 100%
);
font-family: var(--mono);
font-size: 12px;
line-height: 1.7;
color: var(--text);
overflow-x: auto;
}
.mcp-json-helper-code code {
background: transparent;
padding: 0;
border-radius: 0;
}
.json-key {
color: var(--accent);
}
.json-string {
color: var(--green);
}
.json-punctuation {
color: var(--text-soft);
}
.mcp-json-helper-conversion {
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px;
border-top: 1px solid var(--border);
background: transparent;
}
.mcp-json-helper-conversion div {
display: flex;
flex-direction: column;
gap: 4px;
}
.mcp-json-helper-conversion strong {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--text-soft);
}
.mcp-json-helper-conversion code {
width: fit-content;
}
.mcp-json-helper-toggle-content {
display: flex;
align-items: flex-start;
gap: 10px;
min-width: 0;
flex: 1;
}
.mcp-json-helper-eye {
width: 22px;
height: 22px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
background: var(--accent-tint);
color: var(--accent);
flex-shrink: 0;
}
.mcp-json-helper-toggle-text {
font-size: 12px;
font-weight: 500;
line-height: 1.5;
color: inherit;
text-align: left;
}

View file

@ -0,0 +1,432 @@
/* ==================================================================
Plugin detail premium chrome
------------------------------------------------------------------
Shared visual language for the four detail variants:
- Smooth scale-in entry (used by every detail backdrop + modal)
- Trust-tinted byline strip (PluginByline) reused across surfaces
- Share popover (PluginShareMenu) reused across surfaces
- Cinematic media detail layout
The goal: detail modals feel like curated content viewers users
want to share, not like internal admin inspectors.
================================================================== */
@keyframes plugin-detail-fade {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes plugin-detail-rise {
from { opacity: 0; transform: translateY(12px) scale(0.985); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.plugin-details-modal-backdrop,
.ds-modal-backdrop,
.prompt-template-modal-backdrop {
animation: plugin-detail-fade 160ms ease-out;
}
.plugin-details-modal,
.ds-modal,
.prompt-template-modal {
animation: plugin-detail-rise 220ms cubic-bezier(0.16, 0.84, 0.44, 1);
}
.plugin-details-modal__head-actions {
display: inline-flex;
align-items: center;
gap: 6px;
}
/* PluginShareMenu popover trigger that doubles as a regular ghost
pill in the PreviewModal action row, or a softer brand chip when
it stands alone next to the close button on a detail modal. */
.plugin-share-menu {
position: relative;
display: inline-flex;
}
.plugin-share-trigger {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 500;
border-radius: 999px;
padding: 5px 11px;
cursor: pointer;
transition: background-color 120ms ease, color 120ms ease,
border-color 120ms ease;
}
.plugin-share-trigger--solo {
border: 1px solid var(--border);
background: var(--bg-panel);
color: var(--text);
}
.plugin-share-trigger--solo:hover,
.plugin-share-trigger--solo[aria-expanded='true'] {
background: var(--accent-tint);
border-color: var(--accent);
color: var(--accent);
}
.plugin-share-popover {
position: absolute;
top: calc(100% + 6px);
right: 0;
z-index: 60;
min-width: 220px;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: 10px;
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.18);
padding: 6px;
display: flex;
flex-direction: column;
gap: 4px;
animation: plugin-detail-rise 140ms ease-out;
}
.plugin-share-popover__group {
display: flex;
flex-direction: column;
gap: 2px;
}
.plugin-share-popover__divider {
height: 1px;
margin: 4px 4px;
background: var(--border);
opacity: 0.6;
}
.plugin-share-item {
appearance: none;
display: inline-flex;
align-items: center;
gap: 8px;
width: 100%;
border: 0;
background: transparent;
border-radius: 6px;
padding: 7px 9px;
font-size: 12.5px;
color: var(--text);
cursor: pointer;
text-align: left;
text-decoration: none;
}
.plugin-share-item:hover,
.plugin-share-item:focus-visible {
background: var(--bg-subtle);
outline: none;
}
/* PluginByline author avatar + name + trust tier + version chip,
shared by media + scenario variants, lives just under the modal
title bar. The compact variant collapses the secondary link row
when used inside dark/gradient headers. */
.plugin-byline {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 20px;
border-bottom: 1px solid var(--border);
background: var(--bg-panel);
}
.plugin-byline--compact {
padding: 8px 16px;
border-bottom: none;
background: transparent;
}
.plugin-byline__avatar {
width: 34px;
height: 34px;
border-radius: 999px;
background: var(--bg-subtle);
display: flex;
align-items: center;
justify-content: center;
font-size: 11.5px;
font-weight: 700;
color: var(--text-muted);
flex-shrink: 0;
overflow: hidden;
object-fit: cover;
}
.plugin-byline__avatar--fallback {
background: linear-gradient(
135deg,
var(--accent-tint) 0%,
var(--bg-subtle) 100%
);
color: var(--accent);
}
.plugin-byline__meta {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
flex: 1;
}
.plugin-byline__primary {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.plugin-byline__by {
font-size: 11.5px;
color: var(--text-faint);
text-transform: lowercase;
}
.plugin-byline__name {
font-size: 13px;
font-weight: 600;
color: var(--text);
}
.plugin-byline__version {
font-size: 11px;
color: var(--text-faint);
font-variant-numeric: tabular-nums;
}
.plugin-byline__secondary {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.plugin-byline__link {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 11.5px;
color: var(--text-muted);
text-decoration: none;
transition: color 120ms ease;
}
.plugin-byline__link:hover {
color: var(--accent);
}
/* Plugins section composite wraps InlinePluginsRail + ContextChipStrip +
PluginInputsForm. Mounted above ChatComposer (strip variant). The
NewProjectPanel mount has been removed: that flow now binds the
default scenario plugin transparently at create time, so the wide
variant has no remaining host. */
.plugins-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.plugins-section__active {
display: flex;
flex-direction: column;
gap: 6px;
padding: 8px 10px;
border-radius: var(--radius-sm, 6px);
background: var(--bg-subtle);
border: 1px solid var(--border);
}
/* Strip rail single-row horizontal scroller, intended for the
ChatComposer mount where vertical space is precious. Cards collapse
to compact pills (title only). */
.inline-plugins-rail--strip {
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
padding: 2px 0 6px;
max-height: 44px;
}
.inline-plugins-rail--strip::-webkit-scrollbar {
height: 6px;
}
.inline-plugins-rail--strip::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
.inline-plugins-rail--strip .inline-plugins-rail__card {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 6px;
max-width: 200px;
padding: 5px 10px;
border: 1px solid var(--border);
border-radius: 999px;
background: var(--bg);
color: var(--text);
font-size: 12px;
line-height: 1.2;
cursor: pointer;
white-space: nowrap;
}
.inline-plugins-rail--strip .inline-plugins-rail__card:hover:not(:disabled) {
border-color: var(--accent);
color: var(--accent);
}
.inline-plugins-rail--strip .inline-plugins-rail__card:disabled {
opacity: 0.55;
cursor: progress;
}
.inline-plugins-rail--strip .inline-plugins-rail__title {
overflow: hidden;
text-overflow: ellipsis;
font-weight: 500;
}
/* Description + trust are hidden inside the strip rail they live
in the marketplace detail page. Keeping the strip compact is the
only way the chat history stays visible. */
.inline-plugins-rail--strip .inline-plugins-rail__desc,
.inline-plugins-rail--strip .inline-plugins-rail__trust {
display: none;
}
.inline-plugins-rail__error {
flex: 0 0 100%;
font-size: 11.5px;
color: var(--danger, #c33);
}
/* Context chip strip small pill list of typed context items the
active plugin contributed. Sits inside `.plugins-section__active`. */
.context-chip-strip {
display: flex;
flex-wrap: wrap;
gap: 4px 6px;
align-items: center;
}
.context-chip-strip__empty {
font-size: 11.5px;
color: var(--text-muted);
}
.context-chip-strip__chip {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 0 2px 0 0;
border-radius: 999px;
background: var(--bg);
border: 1px solid var(--border);
font-size: 11px;
color: var(--text);
max-width: 240px;
}
/* Per-kind accent stripe keeps the chip recognisable at a glance
without leaning on color-only differentiation (the icon does most
of the work). Uses border-left for skill, design-system, etc. */
.context-chip-strip__chip[data-kind='skill'] {
border-left: 3px solid var(--accent, #6e7bff);
}
.context-chip-strip__chip[data-kind='design-system'] {
border-left: 3px solid #d6a648;
}
.context-chip-strip__chip[data-kind='craft'] {
border-left: 3px solid #6dbf6d;
}
.context-chip-strip__chip[data-kind='asset'] {
border-left: 3px solid #8a8a8a;
}
.context-chip-strip__chip[data-kind='mcp'] {
border-left: 3px solid #c47bd9;
}
.context-chip-strip__chip[data-kind='claude-plugin'] {
border-left: 3px solid #d97757;
}
.context-chip-strip__chip[data-kind='atom'] {
border-left: 3px solid #5fb8d1;
}
.context-chip-strip__chip[data-kind='plugin'] {
border-left: 3px solid var(--accent, #6e7bff);
}
.context-chip-strip__body {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 6px 2px 8px;
background: transparent;
border: none;
color: inherit;
font: inherit;
cursor: default;
text-align: left;
border-radius: 999px;
min-width: 0;
}
.context-chip-strip__chip.is-interactive .context-chip-strip__body {
cursor: pointer;
}
.context-chip-strip__chip.is-interactive .context-chip-strip__body:hover {
background: var(--bg-hover, rgba(255, 255, 255, 0.04));
}
.context-chip-strip__icon {
display: inline-flex;
align-items: center;
color: var(--text-muted);
}
.context-chip-strip__chip.is-interactive .context-chip-strip__body:hover
.context-chip-strip__icon {
color: var(--accent, var(--text));
}
.context-chip-strip__kind {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.03em;
color: var(--text-muted);
}
.context-chip-strip__label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.context-chip-strip__remove {
appearance: none;
background: transparent;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 13px;
line-height: 1;
padding: 0 4px;
}
.context-chip-strip__remove:hover {
color: var(--text);
}
/* Plugin inputs form appears when the active plugin declares
`od.inputs[]`. Compact stacked label/input pairs. */
.plugin-inputs-form {
display: flex;
flex-direction: column;
gap: 6px;
}
.plugin-inputs-form__field {
display: flex;
flex-direction: column;
gap: 3px;
}
.plugin-inputs-form__label {
font-size: 11.5px;
color: var(--text-muted);
}
.plugin-inputs-form__required {
color: var(--danger, #c33);
margin-left: 2px;
}
.plugin-inputs-form__input {
font: inherit;
font-size: 12.5px;
padding: 5px 8px;
border: 1px solid var(--border);
border-radius: var(--radius-sm, 6px);
background: var(--bg);
color: var(--text);
}
.plugin-inputs-form__input:focus {
outline: 1px solid var(--accent);
outline-offset: 0;
border-color: var(--accent);
}
.plugin-inputs-form__input--textarea {
min-height: 56px;
resize: vertical;
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,875 @@
/*
* Inline rendering of Claude's AskUserQuestion tool. Question text reads
* like a normal assistant message, options live as a row of chip buttons
* the user picks from. Once an answer ships (or the user has moved past
* the turn) the card locks into a read only state: chips remain visible
* but unclickable so the user can see what was asked.
*/
.op-ask-question .op-card-head { padding-bottom: 4px; }
.op-ask-question-body {
display: flex;
flex-direction: column;
gap: 14px;
padding: 4px 12px 12px;
}
.op-ask-question-field {
display: flex;
flex-direction: column;
gap: 8px;
}
.op-ask-question-header {
font-size: 10.5px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-muted);
}
.op-ask-question-prompt {
font-size: 13px;
line-height: 1.45;
color: var(--text);
}
.op-ask-question-options {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.op-ask-question-option {
display: inline-flex;
flex-direction: column;
align-items: flex-start;
gap: 2px;
padding: 7px 11px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg-panel);
color: var(--text);
font-size: 12.5px;
font-family: inherit;
cursor: pointer;
text-align: left;
transition: border-color 120ms ease, background 120ms ease;
}
.op-ask-question-option:hover { border-color: var(--accent); }
.op-ask-question-option.on {
border-color: var(--accent);
background: var(--accent-bg, var(--bg-subtle));
}
.op-ask-question-option:disabled {
cursor: default;
opacity: 0.85;
}
.op-ask-question-option-label { font-weight: 500; }
.op-ask-question-option-desc {
font-size: 11px;
color: var(--text-muted);
white-space: normal;
}
.op-ask-question-foot {
display: flex;
justify-content: flex-end;
padding: 4px 12px 12px;
}
/* Match the composer's Send button styling so the two actions read as
* the same control family. The composer-send is bordered with the
* accent color and uses the same padding/typography; we drop the icon
* by skipping the gap/svg children entirely. */
.op-ask-question-submit {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--accent);
border: 1px solid var(--accent);
border-radius: var(--radius);
color: white;
font-weight: 500;
padding: 4px 14px;
font-size: 12.5px;
cursor: pointer;
}
.op-ask-question-submit:hover:not(:disabled) {
background: var(--accent-hover);
border-color: var(--accent-hover);
}
.op-ask-question-submit:disabled {
opacity: 0.5;
cursor: default;
}
/*
* Locked card state: unchosen options fade back into the card surface
* while the user's pick keeps its accent outline so the answer stays
* legible after submission. Without the `:not(.on)` qualifier the lock
* would wipe the selected state and the card would look unanswered.
*/
.op-ask-question-locked .op-ask-question-option:not(.on) {
border-color: var(--border-soft);
background: transparent;
color: var(--text-muted);
}
.op-ask-question-locked .op-ask-question-option.on {
border-color: var(--accent);
background: var(--accent-bg, var(--bg-subtle));
cursor: default;
}
.op-ask-question-locked .op-ask-question-option.on .op-ask-question-option-desc {
color: var(--text-muted);
}
.op-meta { color: var(--text-muted); font-size: 11.5px; }
.op-desc { font-style: italic; }
.op-path {
background: var(--bg-subtle);
padding: 1px 6px;
border-radius: 4px;
font-size: 11px;
font-family: var(--mono);
color: var(--text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: min(220px, 100%);
}
.op-status {
margin-left: auto;
font-size: 10.5px;
padding: 2px 9px;
border-radius: var(--radius-pill);
letter-spacing: 0.02em;
border: 1px solid var(--border);
color: var(--text-muted);
background: var(--bg-subtle);
}
.op-status-running {
border-color: var(--purple-border);
background: var(--purple-bg);
color: var(--purple);
animation: pulse 1.6s ease-in-out infinite;
}
/*
* Static waiting-for-input variant. Same purple chrome as `op-status-running`
* but no pulse: an AskUserQuestion card is parked until the user clicks, not
* actively working, so the animation is misleading.
*/
.op-status-awaiting {
border-color: var(--purple-border);
background: var(--purple-bg);
color: var(--purple);
}
.op-status-ok { border-color: var(--green-border); background: var(--green-bg); color: var(--green); }
.op-status-error { border-color: var(--red-border); background: var(--red-bg); color: var(--red); }
.op-toggle {
font-size: 10.5px;
padding: 2px 8px;
border: 1px solid var(--border);
border-radius: var(--radius-pill);
background: transparent;
color: var(--text-muted);
}
.op-open {
font-size: 10.5px;
padding: 2px 9px;
border: 1px solid var(--border);
border-radius: var(--radius-pill);
background: transparent;
color: var(--text);
cursor: pointer;
}
.op-open:hover {
background: var(--bg-subtle);
border-color: var(--accent);
color: var(--accent);
}
.produced-files {
margin-top: 4px;
padding: 12px 14px;
background: var(--bg-subtle);
border: 1px solid var(--border);
border-radius: var(--radius);
}
.produced-files-label {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
margin-bottom: 8px;
font-weight: 600;
}
.produced-files-list { display: flex; flex-direction: column; gap: 4px; }
.produced-file {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 6px;
background: var(--bg-panel);
border: 1px solid var(--border);
font-size: 12px;
}
.produced-file-icon { width: 22px; text-align: center; color: var(--text-muted); }
.produced-file-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--mono);
}
.produced-file-size {
font-size: 10.5px;
color: var(--text-muted);
font-variant-numeric: tabular-nums;
}
.produced-file-actions { display: flex; gap: 6px; }
.produced-file-actions .ghost,
.produced-file-actions .ghost-link {
font-size: 11px;
padding: 3px 9px;
}
.plugin-action-panel {
margin-top: 4px;
padding: 12px 14px;
border: 1px solid var(--accent-soft);
border-radius: var(--radius);
background: color-mix(in srgb, var(--accent) 5%, var(--bg-panel));
box-shadow: var(--shadow-xs);
}
.plugin-action-panel__head {
display: flex;
align-items: flex-start;
gap: 9px;
margin-bottom: 10px;
}
.plugin-action-panel__icon {
width: 25px;
height: 25px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 7px;
background: var(--accent-soft);
color: var(--accent);
flex-shrink: 0;
}
.plugin-action-panel__title {
font-weight: 650;
font-size: 13px;
color: var(--text);
}
.plugin-action-panel__subtitle {
margin-top: 2px;
font-size: 11.5px;
line-height: 1.35;
color: var(--text-muted);
}
.plugin-action-panel__list {
display: flex;
flex-direction: column;
gap: 8px;
}
.plugin-action-card {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 0;
padding-top: 8px;
border-top: 1px solid var(--border-soft);
}
.plugin-action-panel__list .plugin-action-card:first-child {
padding-top: 0;
border-top: none;
}
.plugin-action-card__main {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.plugin-action-card__folder-icon {
width: 23px;
height: 23px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 6px;
background: var(--bg-subtle);
color: var(--text-muted);
flex-shrink: 0;
}
.plugin-action-card__copy {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 5px 8px;
min-width: 0;
font-size: 11.5px;
color: var(--text-muted);
}
.plugin-action-card__path {
max-width: 220px;
padding: 1px 6px;
border-radius: 4px;
background: var(--bg-panel);
border: 1px solid var(--border-soft);
color: var(--text);
font-family: var(--mono);
font-size: 11px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.plugin-action-card__actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.plugin-action-button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
min-height: 28px;
max-width: 100%;
padding: 5px 10px;
border: 1px solid var(--border);
border-radius: 7px;
background: var(--bg-panel);
color: var(--text);
font-size: 11.5px;
font-weight: 550;
cursor: pointer;
}
.plugin-action-button:hover:not(:disabled) {
border-color: var(--accent);
color: var(--accent);
}
.plugin-action-button:disabled {
opacity: 0.58;
cursor: default;
}
.plugin-action-button--primary {
border-color: var(--accent);
background: var(--accent);
color: #fff;
}
.plugin-action-button--primary:hover:not(:disabled) {
color: #fff;
filter: brightness(0.98);
}
.plugin-action-card__notice {
padding: 6px 8px;
border-radius: 6px;
background: var(--bg-panel);
border: 1px solid var(--border-soft);
color: var(--text-muted);
font-size: 11.5px;
line-height: 1.35;
}
.plugin-action-card__notice a {
color: var(--accent);
text-decoration: underline;
text-underline-offset: 2px;
word-break: break-all;
}
/* "Files this turn" summary derived from Read/Write/Edit tool_use events
so users can scan every file the agent touched without expanding tool-
group disclosures. Compact pill while streaming, full list once done. */
.file-ops {
display: flex;
flex-direction: column;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg-panel);
overflow: hidden;
}
.file-ops.is-streaming {
border-style: dashed;
background: var(--bg-subtle);
}
.file-ops-toggle {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 7px 10px;
background: transparent;
border: 0;
cursor: pointer;
text-align: left;
font-size: 12px;
color: var(--text);
min-width: 0;
}
.file-ops-toggle:hover { background: var(--bg-subtle); }
.file-ops-icon { display: inline-flex; color: var(--text-muted); }
.file-ops-label {
font-size: 10.5px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
font-weight: 600;
}
.file-ops-summary-line {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-variant-numeric: tabular-nums;
color: var(--text-muted);
}
.file-ops-count {
font-size: 10.5px;
font-weight: 600;
font-variant-numeric: tabular-nums;
color: var(--text-muted);
padding: 1px 6px;
border-radius: 8px;
background: var(--bg-subtle);
border: 1px solid var(--border);
}
.file-ops.is-streaming .file-ops-count {
color: var(--accent);
border-color: color-mix(in oklab, var(--accent) 30%, var(--border));
}
.file-ops-chev { display: inline-flex; color: var(--text-muted); }
.file-ops-list {
list-style: none;
margin: 0;
padding: 4px 6px 8px;
display: flex;
flex-direction: column;
gap: 3px;
border-top: 1px solid var(--border-soft, var(--border));
}
.file-ops-row {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 8px;
border-radius: 6px;
font-size: 12px;
min-width: 0;
}
.file-ops-row:hover { background: var(--bg-subtle); }
.file-ops-row--running { color: var(--text); }
.file-ops-row--error { color: var(--text); }
.file-ops-row-badges { display: inline-flex; gap: 3px; flex-shrink: 0; }
.file-ops-badge {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 2px;
min-width: 18px;
height: 18px;
padding: 0 4px;
border-radius: 4px;
font-size: 10px;
font-weight: 700;
font-family: var(--mono);
letter-spacing: 0;
border: 1px solid var(--border);
background: var(--bg-subtle);
color: var(--text-muted);
}
.file-ops-badge-count {
font-size: 9px;
font-weight: 600;
font-variant-numeric: tabular-nums;
opacity: 0.85;
}
.file-ops-badge--read {
color: color-mix(in oklab, var(--accent) 80%, var(--text));
border-color: color-mix(in oklab, var(--accent) 30%, var(--border));
}
.file-ops-badge--write {
color: #2e7d32;
border-color: color-mix(in oklab, #2e7d32 35%, var(--border));
background: color-mix(in oklab, #2e7d32 8%, var(--bg-subtle));
}
.file-ops-badge--edit {
color: #b26500;
border-color: color-mix(in oklab, #b26500 35%, var(--border));
background: color-mix(in oklab, #b26500 8%, var(--bg-subtle));
}
.file-ops-row-path {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-family: var(--mono);
font-size: 11.5px;
background: transparent;
padding: 0;
}
.file-ops-row-count {
font-size: 10.5px;
color: var(--text-muted);
font-variant-numeric: tabular-nums;
flex-shrink: 0;
}
.file-ops-row-status {
font-size: 10.5px;
text-transform: uppercase;
letter-spacing: 0.04em;
font-weight: 600;
padding: 1px 6px;
border-radius: 8px;
flex-shrink: 0;
}
.file-ops-row-status--running {
color: var(--accent);
background: color-mix(in oklab, var(--accent) 10%, transparent);
}
.file-ops-row-status--error {
color: #c0392b;
background: color-mix(in oklab, #c0392b 12%, transparent);
}
.file-ops-row-open {
font-size: 11px;
padding: 3px 9px;
background: transparent;
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
cursor: pointer;
flex-shrink: 0;
}
.file-ops-row-open:hover {
border-color: var(--accent);
color: var(--accent);
}
.op-bash .op-command,
.op-bash .op-output {
margin: 0;
padding: 8px 12px;
background: #1c1b1a;
color: #f0eee9;
font-family: var(--mono);
font-size: 11px;
line-height: 1.55;
white-space: pre-wrap;
overflow-wrap: anywhere;
max-height: 220px;
overflow-y: auto;
}
.op-bash .op-output { background: #2a2926; }
.op-todo .todo-list {
list-style: none;
margin: 0;
padding: 6px 8px 10px;
display: flex;
flex-direction: column;
gap: 1px;
font-size: 12px;
}
.todo-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 5px 8px;
line-height: 1.45;
border-radius: var(--radius-sm);
border: 1px solid transparent;
transition: background-color 120ms ease, border-color 120ms ease;
}
.todo-check {
width: 16px;
flex-shrink: 0;
color: var(--text-soft);
font-family: var(--mono);
text-align: center;
line-height: 1.45;
}
.todo-text { color: var(--text); }
/* Pending — quietly waiting */
.todo-pending .todo-check { color: var(--text-faint); }
.todo-pending .todo-text { color: var(--text-muted); }
/* In progress — the only row that should pop */
.todo-in_progress {
background: color-mix(in srgb, var(--accent) 10%, var(--bg-panel));
border-color: color-mix(in srgb, var(--accent) 28%, transparent);
}
.todo-in_progress .todo-check { color: var(--accent); }
.todo-in_progress .todo-text { color: var(--text-strong); font-weight: 600; }
/* Completed — settled, but still readable */
.todo-completed .todo-check { color: var(--green); }
.todo-completed .todo-text {
text-decoration: line-through;
text-decoration-color: color-mix(in srgb, var(--text-muted) 60%, transparent);
color: var(--text-muted);
}
/* Stopped — terminal fallback for a run that ended mid-task */
.todo-stopped {
background: color-mix(in srgb, var(--red) 8%, var(--bg-panel));
border-color: color-mix(in srgb, var(--red) 24%, transparent);
}
.todo-stopped .todo-check { color: var(--red); font-weight: 700; }
.todo-stopped .todo-text { color: var(--text); }
/* Composer extras */
.composer.drag-active {
outline: 2px dashed var(--accent);
outline-offset: -4px;
}
/* Present / Share menus */
.present-wrap { position: relative; display: inline-block; }
.present-trigger .caret {
margin-left: 4px;
font-size: 10px;
opacity: 0.7;
}
.present-menu {
position: absolute;
top: calc(100% + 6px);
right: 0;
z-index: 60;
min-width: 168px;
padding: 4px;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow-md);
display: flex;
flex-direction: column;
}
.present-menu button {
background: transparent;
border: none;
padding: 8px 10px;
font-size: 12px;
text-align: left;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
color: var(--text);
}
.present-menu button:hover { background: var(--bg-subtle); border-color: transparent; }
.present-icon {
display: inline-flex;
width: 14px;
justify-content: center;
color: var(--text-muted);
font-size: 12px;
}
.share-menu { position: relative; display: inline-block; }
.viewer-action-export {
gap: 6px;
background: var(--accent);
border-color: var(--accent);
color: white;
box-shadow: 0 6px 14px color-mix(in srgb, var(--accent) 20%, transparent);
}
.viewer-action-export:hover:not(:disabled) {
background: var(--accent-hover);
border-color: var(--accent-hover);
color: white;
}
@keyframes export-ready-nudge {
0% {
transform: translateY(0);
box-shadow: 0 6px 14px color-mix(in srgb, var(--accent) 24%, transparent);
}
18% {
transform: translateY(-1px);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent),
0 8px 18px color-mix(in srgb, var(--accent) 30%, transparent);
}
36% {
transform: translateY(0);
}
54% {
transform: translateY(-1px);
box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent) 12%, transparent),
0 8px 18px color-mix(in srgb, var(--accent) 28%, transparent);
}
100% {
transform: translateY(0);
box-shadow: 0 6px 14px color-mix(in srgb, var(--accent) 24%, transparent);
}
}
@media (prefers-reduced-motion: reduce) {
.chrome-action-export.export-ready-nudge {
animation: export-ready-nudge-reduced 1200ms ease-out 1;
}
}
@keyframes export-ready-nudge-reduced {
0%, 100% {
box-shadow: 0 6px 14px color-mix(in srgb, var(--accent) 24%, transparent);
}
35% {
box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 18%, transparent);
}
}
.share-menu-popover {
position: absolute;
top: calc(100% + 6px);
right: 0;
z-index: 50;
min-width: 240px;
padding: 4px;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow-md);
display: flex;
flex-direction: column;
}
.share-menu-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
font-size: 12.5px;
text-align: left;
background: transparent;
border: none;
border-radius: 6px;
cursor: pointer;
color: var(--text);
}
.share-menu-item:hover:not(:disabled) {
background: var(--bg-subtle);
border-color: transparent;
}
.share-menu-item:disabled { opacity: 0.45; cursor: not-allowed; }
.share-menu-icon { flex: 0 0 auto; width: 18px; text-align: center; font-size: 13px; }
.share-menu-divider { height: 1px; background: var(--border); margin: 4px 6px; }
.button-like {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 36px;
padding: 0 14px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg-panel);
color: var(--text);
text-decoration: none;
cursor: pointer;
}
.button-like:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.deploy-modal {
width: min(760px, calc(100vw - 32px));
max-height: calc(100vh - 32px);
overflow: auto;
}
/* Add a few extra pixels above the deploy dialog's primary action so
it does not crowd the divider. The shared .modal-foot uses 12px
vertical padding which is fine for shorter dialogs but reads as
cramped here because the deploy form ends in dense token / domain
config rows that push content right up to the border. Scoped to the
deploy dialog only (the template-save dialog also carries the
.deploy-modal class on its root, which is why we key off the
purpose-specific .deploy-flow-modal hook instead). 16px matches the
bump proposed for the global rule in #957 so the rhythm stays close
to the rest of the app. Issue #913. */
.deploy-flow-modal .modal-foot {
padding-top: 16px;
}
.deploy-form {
display: flex;
flex-direction: column;
gap: 14px;
margin-top: 18px;
}
.deploy-provider-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.field-label-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16px;
}
.field-label-note {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
max-width: min(420px, 70%);
text-align: right;
}
.field-label-note .hint {
margin: 0;
font-size: 12px;
line-height: 1.35;
}
.field-label-row a:not(.field-label-link) {
color: var(--accent);
font-size: 13px;
white-space: nowrap;
}
.field-label-group {
display: inline-flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.field-label-link {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 10.5px;
color: var(--text-muted);
text-decoration: none;
white-space: nowrap;
transition: color 120ms ease;
}
.field-label-link:hover {
color: var(--accent);
}
.field-label-link svg {
opacity: 0.75;
transition: transform 120ms ease, opacity 120ms ease;
}
.field-label-link:hover svg {
opacity: 1;
transform: translate(1px, -1px);
}
.field-status-badge {
display: inline-flex;
align-items: center;
font-size: 10.5px;
font-weight: 600;
letter-spacing: 0.02em;
padding: 2px 7px;
border-radius: var(--radius-pill);
color: #137a3d;
background: color-mix(in srgb, #1f9d55 10%, transparent);
border: 1px solid color-mix(in srgb, #1f9d55 28%, var(--border));
font-variant-numeric: tabular-nums;
}
/*
* Inline variant lives next to a label on the same row instead of taking
* its own line. We also drop the green "success" treatment because the
* media-provider row already has a green "Integrated" badge two green
* pills in one row read as duplicate state. Neutral muted gray keeps it
* legible as metadata without competing for attention.
*/
.field-status-badge--inline {
color: var(--text-muted);
background: var(--bg-subtle);
border-color: var(--border);
font-weight: 500;
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,12 @@
// @vitest-environment jsdom
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { ChatComposer } from '../../src/components/ChatComposer';
import { ANNOTATION_EVENT } from '../../src/components/PreviewDrawOverlay';
import { uploadProjectFiles } from '../../src/providers/registry';
import { readExpandedIndexCss } from '../helpers/read-expanded-css';
import type { ChatAttachment, ChatCommentAttachment } from '../../src/types';
vi.mock('../../src/providers/registry', async () => {
@ -355,7 +354,7 @@ describe('ChatComposer /search command', () => {
});
it('keeps staged image preview modal styling available', () => {
const css = readFileSync(join(process.cwd(), 'src/index.css'), 'utf8');
const css = readExpandedIndexCss();
expect(css).toContain('.staged-preview-modal');
expect(css).toContain('position: fixed;');

View file

@ -38,6 +38,7 @@ import type { InspectOverrideMap } from '../../src/components/FileViewer';
import type { LiveArtifact, LiveArtifactWorkspaceEntry, PreviewComment, ProjectFile } from '../../src/types';
import { I18nProvider } from '../../src/i18n';
import type { Dict } from '../../src/i18n/types';
import { readExpandedIndexCss } from '../helpers/read-expanded-css';
afterEach(() => {
cleanup();
@ -78,6 +79,14 @@ function srcDocActivationMessages(calls: readonly (readonly unknown[])[]) {
}
describe('FileViewer preview scale', () => {
it('keeps file viewer selectors in the effective global stylesheet', () => {
const css = readExpandedIndexCss();
expect(css).toContain('.viewer');
expect(css).toContain('.viewer-toolbar');
expect(css).toContain('.viewer-action');
});
it('uses the requested zoom for desktop preview overlays', () => {
expect(effectivePreviewScale('desktop', 1.5, { width: 320, height: 480 })).toBe(1.5);
});
@ -2512,14 +2521,14 @@ function baseLiveArtifactWorkspaceEntry(
describe('LiveArtifactViewer', () => {
it('hides inactive live previews even when a device viewport sets display', () => {
const css = readFileSync(join(process.cwd(), 'src/index.css'), 'utf8');
const css = readExpandedIndexCss();
const rule = css.match(/\.live-artifact-preview-layer\.preview-viewport\[data-active='false'\]\s*\{[^}]+\}/)?.[0] ?? '';
expect(rule).toContain('display: none;');
});
it('keeps the presentation exit button aligned with preview chrome spacing', () => {
const css = readFileSync(join(process.cwd(), 'src/index.css'), 'utf8');
const css = readExpandedIndexCss();
const rule = css.match(/\.present-exit\s*\{[^}]+\}/)?.[0] ?? '';
expect(rule).toContain('top: calc(env(safe-area-inset-top, 0px) + 20px);');

View file

@ -1,10 +1,9 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
import { readExpandedIndexCss } from '../../helpers/read-expanded-css';
describe('Critique Theater styles', () => {
it('keeps the Theater UI selectors in the global stylesheet', () => {
const css = readFileSync(join(process.cwd(), 'src/index.css'), 'utf8');
const css = readExpandedIndexCss();
expect(css).toContain('.theater-stage');
expect(css).toContain('.theater-lane');

View file

@ -0,0 +1,23 @@
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
function expandCssFile(filePath: string, seen = new Set<string>()): string {
const key = filePath;
if (seen.has(key)) {
return '';
}
seen.add(key);
const css = readFileSync(filePath, 'utf8');
return css.replace(/@import\s+(?:url\(([^)]+)\)|(['"])([^'"]+)\2);/g, (_match, urlImport, _quote, quotedImport) => {
const specifier = (quotedImport ?? urlImport ?? '').trim().replace(/^['"]|['"]$/g, '');
if (!specifier.startsWith('./') && !specifier.startsWith('../')) {
return '';
}
return expandCssFile(join(dirname(filePath), specifier), seen);
});
}
export function readExpandedIndexCss(): string {
return expandCssFile(join(process.cwd(), 'src/index.css'));
}

View file

@ -2,8 +2,9 @@ import { readFileSync } from 'node:fs';
import postcss, { type Declaration, type Root, type Rule } from 'postcss';
import { describe, expect, it } from 'vitest';
import { readExpandedIndexCss } from '../helpers/read-expanded-css';
const indexCss = readFileSync(new URL('../../src/index.css', import.meta.url), 'utf8');
const indexCss = readExpandedIndexCss();
const tasksCss = readFileSync(
new URL('../../src/styles/home/tasks.css', import.meta.url),
'utf8',

View file

@ -1,7 +1,7 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
import { readExpandedIndexCss } from '../helpers/read-expanded-css';
const indexCss = readFileSync(new URL('../../src/index.css', import.meta.url), 'utf8');
const indexCss = readExpandedIndexCss();
function cssBlock(selector: string): string {
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

View file

@ -1,7 +1,7 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
import { readExpandedIndexCss } from '../helpers/read-expanded-css';
const indexCss = readFileSync(new URL('../../src/index.css', import.meta.url), 'utf8');
const indexCss = readExpandedIndexCss();
function cssBlock(selector: string): string {
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

View file

@ -1,12 +1,10 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import postcss, { type Rule } from 'postcss';
import { describe, expect, it } from 'vitest';
import { readExpandedIndexCss } from '../helpers/read-expanded-css';
describe('plugin info preview pane styles', () => {
it('keeps plugin preview sidebar content away from the pane edges', () => {
const css = readFileSync(join(process.cwd(), 'src/index.css'), 'utf8');
const css = readExpandedIndexCss();
const root = postcss.parse(css, { from: 'src/index.css' });
const topLevelRules = root.nodes.filter(
(node): node is Rule => node.type === 'rule',

View file

@ -1,11 +1,9 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
import { readExpandedIndexCss } from '../helpers/read-expanded-css';
describe('plugin share confirmation styles', () => {
it('keeps the publish dialog footer away from the modal edge', () => {
const css = readFileSync(join(process.cwd(), 'src/index.css'), 'utf8');
const css = readExpandedIndexCss();
expect(css).toContain('.plugin-share-confirm .plugin-details-modal__foot');
expect(css).toContain('padding: 16px 24px 22px;');

View file

@ -1,7 +1,8 @@
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
import { readExpandedIndexCss } from '../helpers/read-expanded-css';
const indexCss = readFileSync(new URL('../../src/index.css', import.meta.url), 'utf8');
const indexCss = readExpandedIndexCss();
const pluginsHomeCss = readFileSync(
new URL('../../src/styles/home/plugins-home.css', import.meta.url),
'utf8',