/* ============================================================================ */ /* Editor Chrome UI Components */ /* Themed editor shell using CSS custom properties. */ /* ============================================================================ */ :root, [data-theme="light"] { /* Editor shell matches the article page so there's no grey frame around the content. Surfaces stay white and hover uses a subtle grey. */ --ed-bg: #ffffff; --ed-surface: #ffffff; --ed-surface-hover: #f0f0f0; --ed-text: #1a1a1a; --ed-text-secondary: #666; --ed-text-disabled: #999; --ed-border: #ddd; --ed-radius: 8px; --ed-radius-sm: 6px; --ed-success: #2e7d32; --ed-error: #d32f2f; } [data-theme="dark"] { --ed-bg: #0f0f0f; --ed-surface: #1a1a1a; --ed-surface-hover: #222; --ed-text: #e0e0e0; --ed-text-secondary: #888; --ed-text-disabled: #555; --ed-border: #2a2a2a; --ed-success: #66bb6a; --ed-error: #f44336; } @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --ed-bg: #0f0f0f; --ed-surface: #1a1a1a; --ed-surface-hover: #222; --ed-text: #e0e0e0; --ed-text-secondary: #888; --ed-text-disabled: #555; --ed-border: #2a2a2a; --ed-success: #66bb6a; --ed-error: #f44336; } } /* ---- Icon button ---- */ .icon-btn { display: inline-flex; align-items: center; justify-content: center; border: none; background: none; cursor: pointer; padding: 6px; border-radius: var(--ed-radius-sm); color: var(--ed-text-disabled); transition: color 0.15s, background-color 0.15s; line-height: 0; } .icon-btn:hover { color: var(--ed-text); background: var(--ed-surface-hover); } .icon-btn:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; } .icon-btn svg { width: 18px; height: 18px; } .icon-btn--active { color: var(--primary-color); } .icon-btn--primary { color: var(--primary-color); } /* ---- Button ---- */ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; border: none; background: none; cursor: pointer; padding: 6px 14px; border-radius: var(--ed-radius-sm); font-size: 0.85rem; font-weight: 500; font-family: inherit; color: var(--ed-text-secondary); transition: all 0.15s; } .btn:hover { color: var(--ed-text); background: var(--ed-surface-hover); } .btn:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; } .btn--primary { background: var(--primary-color); color: #000; font-weight: 600; } .btn--primary:hover { background: var(--primary-color-hover); color: #000; } .btn--primary:disabled { opacity: 0.5; cursor: not-allowed; } /* ---- Chip ---- */ .chip { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 999px; font-size: 0.65rem; font-weight: 600; line-height: 1; height: 22px; white-space: nowrap; } .chip--sm { height: 18px; padding: 1px 6px; } .chip--outlined { border: 1px solid var(--ed-border); background: transparent; } .chip--clickable { cursor: pointer; } .chip--login { background: var(--ed-surface-hover); color: var(--ed-text); cursor: pointer; transition: background 0.15s; } .chip--login:hover { background: var(--ed-border); } .chip--clickable:hover { filter: brightness(1.1); } .chip img { width: 18px; height: 18px; border-radius: 50%; object-fit: cover; } /* Access badges shown next to the user chip when authenticated. --editor confirms write access; --readonly signals the user is signed in but lacks a write role on the backing Space (so mutating actions are disabled). Both stay subtle so they don't compete with the colored user chip. */ .chip--editor { background: color-mix(in srgb, var(--ed-success) 14%, transparent); color: var(--ed-success); border: 1px solid color-mix(in srgb, var(--ed-success) 40%, transparent); gap: 4px; font-weight: 500; } .chip--readonly { background: var(--ed-surface-hover); color: var(--ed-text-secondary); border: 1px solid var(--ed-border); gap: 4px; font-weight: 500; } /* ---- Surface (replaces MUI Paper) ---- */ .surface { background: var(--ed-surface); border: 1px solid var(--ed-border); border-radius: var(--ed-radius); } /* ---- Dividers ---- */ .divider-v { width: 1px; height: 16px; background: var(--ed-border); margin: 0 4px; flex-shrink: 0; } .divider-h { border: none; height: 1px; background: var(--ed-border); margin: 0; } /* ---- Spinner ---- */ .spinner { display: inline-block; width: 24px; height: 24px; border: 2.5px solid rgba(255, 255, 255, 0.15); border-top-color: var(--primary-color); border-radius: 50%; animation: ed-spin 0.8s linear infinite; } .spinner--lg { width: 36px; height: 36px; border-width: 3px; /* The large spinner is only used in the boot overlay, before Yjs has synced the article's primaryHue. Using --primary-color here would paint the spinner in the default warm-yellow palette for a beat, then snap to the article's overridden hue once the settings map arrives - a jarring color jump. Neutralising the large spinner sidesteps that transition entirely; the "branded" colour only kicks in for post-boot spinners (sync indicator, publish, etc.). */ border-top-color: var(--muted-color); } @keyframes ed-spin { to { transform: rotate(360deg); } } /* ---- Editor loading overlay ---- */ .editor-loading-overlay { position: absolute; inset: 0; z-index: 100; display: flex; align-items: center; justify-content: center; background: var(--ed-bg, #0a0a0a); } .editor-scroll { flex: 1; overflow-y: auto; overflow-x: clip; opacity: 0; transition: opacity 0.3s ease; /* Prevent browser scroll anchoring from "sticking" scroll when iframe heights change or dynamic content shifts layout. */ overflow-anchor: none; position: relative; } .editor-scroll--ready { opacity: 1; } /* ---- Badge dot ---- */ .badge-dot { position: relative; } .badge-dot::after { content: ''; position: absolute; top: 2px; right: 2px; width: 8px; height: 8px; background: var(--ed-error); border-radius: 50%; pointer-events: none; } .badge-dot--hidden::after { display: none; } /* ---- Native dialog ---- */ dialog.ed-dialog { border: 1px solid var(--ed-border); border-radius: var(--ed-radius); background: var(--ed-surface); color: var(--ed-text); padding: 0; max-width: 400px; width: 90vw; } dialog.ed-dialog::backdrop { background: rgba(0, 0, 0, 0.6); } .ed-dialog__title { padding: 16px 20px 8px; font-size: 1.1rem; font-weight: 600; margin: 0; } .ed-dialog__body { padding: 8px 20px 16px; font-size: 0.9rem; color: var(--ed-text-secondary); line-height: 1.6; } .ed-dialog__body p { margin: 0; } .ed-dialog__actions { display: flex; justify-content: flex-end; gap: 8px; padding: 8px 20px 16px; } /* ---- Publish dialog ---- */ dialog.ed-dialog.ed-dialog--publish { max-width: 460px; overflow: hidden; box-shadow: 0 1px 0 rgba(255, 255, 255, 0.04) inset, 0 24px 48px -12px rgba(0, 0, 0, 0.35), 0 8px 18px -8px rgba(0, 0, 0, 0.25); } dialog.ed-dialog.ed-dialog--publish::backdrop { background: rgba(10, 10, 12, 0.55); backdrop-filter: blur(2px); } .publish-dialog__header { display: flex; align-items: flex-start; gap: 14px; padding: 22px 24px 14px; position: relative; } .publish-dialog__header::after { content: ""; position: absolute; left: 24px; right: 24px; bottom: 0; height: 1px; background: var(--ed-border); opacity: 0.6; } .publish-dialog__icon { flex-shrink: 0; width: 40px; height: 40px; border-radius: 10px; display: inline-flex; align-items: center; justify-content: center; background: color-mix(in srgb, var(--primary-color) 14%, transparent); color: var(--primary-color); transition: background 0.25s ease, color 0.25s ease; } .publish-dialog__icon--success { background: color-mix(in srgb, var(--ed-success) 16%, transparent); color: var(--ed-success); } .publish-dialog__icon--error { background: color-mix(in srgb, var(--ed-error) 16%, transparent); color: var(--ed-error); } .publish-dialog__icon--loading .spinner { width: 20px; height: 20px; border-width: 2px; } .publish-dialog__heading { flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0; } .publish-dialog__title { margin: 0; font-size: 1.05rem; font-weight: 600; letter-spacing: -0.005em; color: var(--ed-text); } .publish-dialog__subtitle { margin: 0; font-size: 0.85rem; color: var(--ed-text-secondary); line-height: 1.45; /* Stage name can grow during loading; avoid layout jump. */ min-height: 1.2em; } .publish-dialog__body { padding: 18px 24px 6px; } .publish-dialog__intro { display: flex; flex-direction: column; gap: 14px; } .publish-dialog__checklist { list-style: none; margin: 0; padding: 14px 14px; display: flex; flex-direction: column; gap: 10px; font-size: 0.88rem; color: var(--ed-text); border: 1px solid var(--ed-border); border-radius: var(--ed-radius-sm); background: color-mix(in srgb, var(--primary-color) 5%, transparent); } .publish-dialog__checklist li { display: flex; align-items: flex-start; gap: 10px; line-height: 1.45; } .publish-dialog__checklist strong { color: var(--ed-text); font-weight: 600; } .publish-dialog__bullet { flex-shrink: 0; margin-top: 7px; width: 6px; height: 6px; border-radius: 50%; background: var(--primary-color); box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 18%, transparent); } .publish-dialog__note { margin: 0; display: inline-flex; align-items: center; gap: 6px; font-size: 0.78rem; color: var(--ed-text-disabled); padding-left: 2px; } .publish-dialog__note svg { color: var(--primary-color); opacity: 0.8; } .publish-dialog__progress { display: flex; flex-direction: column; gap: 12px; } .publish-dialog__bar { position: relative; height: 6px; border-radius: 999px; background: var(--ed-surface-hover); overflow: hidden; } .publish-dialog__bar-fill { position: absolute; left: 0; top: 0; bottom: 0; border-radius: 999px; background: linear-gradient( 90deg, color-mix(in srgb, var(--primary-color) 55%, transparent), var(--primary-color) ); transition: width 0.45s cubic-bezier(0.4, 0, 0.2, 1); box-shadow: 0 0 8px color-mix(in srgb, var(--primary-color) 45%, transparent); } .publish-dialog__bar-fill::after { content: ""; position: absolute; inset: 0; background: linear-gradient( 90deg, transparent, rgba(255, 255, 255, 0.18), transparent ); transform: translateX(-100%); animation: publish-shimmer 1.6s ease-in-out infinite; } @keyframes publish-shimmer { to { transform: translateX(100%); } } .publish-dialog__hint { margin: 0; font-size: 0.8rem; color: var(--ed-text-disabled); line-height: 1.5; } .publish-dialog__success { display: flex; flex-direction: column; gap: 14px; } .publish-dialog__success-url { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border: 1px solid var(--ed-border); border-radius: var(--ed-radius-sm); background: color-mix(in srgb, var(--ed-success) 6%, transparent); } .publish-dialog__url-label { font-size: 0.72rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--ed-success); flex-shrink: 0; } .publish-dialog__url-code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 0.82rem; color: var(--ed-text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; background: transparent; padding: 0; } .publish-dialog__view-btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; text-decoration: none; width: 100%; padding: 10px 16px; font-weight: 600; } .publish-dialog__view-btn svg:last-of-type { opacity: 0.8; } .publish-dialog__error { margin: 0; padding: 10px 12px; border-radius: var(--ed-radius-sm); background: color-mix(in srgb, var(--ed-error) 10%, transparent); border: 1px solid color-mix(in srgb, var(--ed-error) 30%, transparent); color: var(--ed-error); font-size: 0.85rem; line-height: 1.5; } .publish-dialog__actions { display: flex; justify-content: flex-end; gap: 8px; padding: 14px 24px 20px; } /* ---- Drawer ---- */ .drawer-backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); z-index: var(--z-overlay, 1000); opacity: 0; pointer-events: none; transition: opacity 0.25s; } .drawer-backdrop.open { opacity: 1; pointer-events: auto; } .drawer-panel { position: fixed; top: 0; right: 0; bottom: 0; width: 380px; background: var(--ed-surface); border-left: 1px solid var(--ed-border); z-index: var(--z-modal, 1100); transform: translateX(100%); transition: transform 0.25s ease; overflow-y: auto; } .drawer-panel.open { transform: translateX(0); } /* ---- Form input / textarea ---- */ .form-input { display: block; width: 100%; padding: 8px 12px; background: var(--ed-bg); border: 1px solid var(--ed-border); border-radius: var(--ed-radius-sm); color: var(--ed-text); font-size: 0.875rem; font-family: inherit; outline: none; transition: border-color 0.15s; } .form-input:focus { border-color: var(--primary-color); } .form-input::placeholder { color: var(--ed-text-disabled); } textarea.form-input { resize: vertical; min-height: 60px; } /* ---- Form select ---- */ .form-select { display: block; width: 100%; padding: 6px 28px 6px 10px; background-color: var(--ed-bg); border: 1px solid var(--ed-border); border-radius: var(--ed-radius-sm); color: var(--ed-text); font-size: 0.875rem; } .form-select:focus { border-color: var(--primary-color); } /* ---- Switch ---- */ .form-switch-label { display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 0.85rem; color: var(--ed-text); } .form-switch { position: relative; display: inline-block; width: 34px; height: 20px; flex-shrink: 0; } .form-switch input { opacity: 0; width: 0; height: 0; position: absolute; } .form-switch__track { position: absolute; inset: 0; background: var(--ed-border); border-radius: 10px; transition: background 0.2s; } .form-switch input:checked + .form-switch__track { background: var(--primary-color); } .form-switch__thumb { position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; background: white; border-radius: 50%; transition: transform 0.2s; pointer-events: none; } .form-switch input:checked ~ .form-switch__thumb { transform: translateX(14px); } .form-switch input:focus-visible + .form-switch__track { outline: 2px solid var(--primary-color); outline-offset: 2px; } /* ---- Field group label ---- */ .field-label { display: block; font-size: 0.75rem; font-weight: 500; color: var(--ed-text-secondary); margin-bottom: 4px; } /* ---- Tooltip ---- */ .ed-tooltip { position: fixed; z-index: var(--z-tooltip, 1200); pointer-events: none; background: #333; color: #fff; font-size: 0.7rem; padding: 4px 8px; border-radius: 4px; white-space: nowrap; line-height: 1.4; } /* ---- Bubble toolbar ---- */ .bubble-toolbar { display: flex; align-items: center; gap: 2px; background: #1a1a1a; border: 1px solid #333; border-radius: 8px; padding: 2px 4px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); } .bubble-toolbar .icon-btn { color: rgba(255, 255, 255, 0.6); padding: 6px; } .bubble-toolbar .icon-btn:hover { color: #fff; background: none; } .bubble-toolbar .icon-btn--active { color: #fff; } .bubble-toolbar .divider-v { height: 16px; background: #444; margin: 0 2px; } /* ---- Floating actions ---- */ .floating-actions { display: flex; align-items: flex-start; gap: 4px; } .floating-actions__toggle { display: inline-flex; align-items: center; justify-content: center; border: 1px solid transparent; background: none; cursor: pointer; padding: 4px; border-radius: var(--ed-radius-sm); color: var(--ed-text-disabled); transition: all 0.15s; line-height: 0; } .floating-actions__toggle:hover { color: var(--ed-text); border-color: var(--ed-border); } .floating-actions__toggle--open { color: var(--ed-text); border-color: var(--ed-border); transform: rotate(45deg); } .floating-actions__toggle svg { width: 20px; height: 20px; } .floating-actions__panel { display: flex; gap: 2px; padding: 2px 4px; animation: fadeIn 0.15s ease; } .floating-actions__panel .icon-btn { color: var(--ed-text-secondary); padding: 6px; } @keyframes fadeIn { from { opacity: 0; transform: translateY(-2px); } to { opacity: 1; transform: translateY(0); } } /* ---- Comment popover (thread popup) ---- */ .comment-popover { width: 320px; background: var(--bg-surface); border: 1px solid var(--border-color); border-radius: 12px; box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35); overflow: hidden; animation: fadeIn 0.12s ease; } .comment-popover__thread { max-height: 280px; overflow-y: auto; padding: 12px 14px 8px; display: flex; flex-direction: column; gap: 10px; } .comment-popover__message { display: flex; flex-direction: column; gap: 2px; } .comment-popover__reply { padding-left: 12px; border-left: 2px solid var(--border-color); } .comment-popover__header { display: flex; align-items: center; gap: 6px; } .comment-popover__author { font-size: 0.8rem; font-weight: 600; } .comment-popover__time { font-size: 0.65rem; color: var(--ed-text-disabled); margin-left: auto; } .comment-popover__text { font-size: 0.8rem; line-height: 1.5; color: var(--ed-text); } .comment-popover__input-row { display: flex; align-items: flex-end; gap: 6px; padding: 8px 14px; border-top: 1px solid var(--border-color); } .comment-popover__input { flex: 1; min-height: 28px; max-height: 80px; padding: 6px 8px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--code-bg); color: var(--ed-text); font-size: 0.8rem; font-family: inherit; line-height: 1.4; resize: none; outline: none; } .comment-popover__input:focus { border-color: var(--border-focus); } .comment-popover__input::placeholder { color: var(--ed-text-disabled); } .comment-popover__send { flex-shrink: 0; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border: none; border-radius: 6px; background: var(--accent-bg); color: var(--accent-light); cursor: pointer; transition: background 0.15s; } .comment-popover__send:hover:not(:disabled) { background: var(--accent-bg-hover); } .comment-popover__send:disabled { opacity: 0.3; cursor: default; } .comment-popover__actions { display: flex; align-items: center; gap: 4px; padding: 6px 14px 10px; } .comment-popover__resolve { display: flex; align-items: center; gap: 4px; padding: 4px 10px; border: none; border-radius: 6px; background: rgba(46, 204, 113, 0.12); color: #2ecc71; font-size: 0.75rem; font-family: inherit; font-weight: 500; cursor: pointer; transition: background 0.15s; } .comment-popover__resolve:hover { background: rgba(46, 204, 113, 0.22); } .comment-popover__delete { display: flex; align-items: center; justify-content: center; width: 26px; height: 26px; margin-left: auto; border: none; border-radius: 6px; background: none; color: var(--ed-text-disabled); cursor: pointer; transition: color 0.15s, background 0.15s; } .comment-popover__delete:hover { color: var(--ed-error); background: rgba(231, 76, 60, 0.1); } /* ---- Comment margin strip (icons + popover) ---- */ .comment-margin-strip { position: absolute; top: 0; right: 12px; width: 28px; height: 100%; pointer-events: none; z-index: 80; } .comment-margin-icon { position: absolute; right: 0; width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background: var(--accent-bg); color: var(--accent-light); cursor: pointer; pointer-events: auto; transition: top 0.2s ease, transform 0.15s, box-shadow 0.15s, background 0.15s; transform: translateY(-50%); } .comment-margin-icon:hover { transform: translateY(-50%) scale(1.12); box-shadow: 0 2px 8px rgba(149, 141, 241, 0.3); background: var(--accent-bg-hover); } .comment-margin-icon--active { background: var(--accent-bg-hover); box-shadow: 0 2px 8px rgba(149, 141, 241, 0.3); } .comment-margin-strip .comment-popover { position: absolute; right: 36px; pointer-events: auto; animation: comment-popover-in 0.15s ease; } @keyframes comment-popover-in { from { opacity: 0; transform: translateX(8px); } to { opacity: 1; transform: translateX(0); } } /* ---- Top bar ---- */ .top-bar { flex-shrink: 0; display: flex; align-items: center; justify-content: flex-end; padding: 6px 16px; gap: 4px; } .top-bar__menu { position: relative; display: inline-flex; } .top-bar__menu-pop { position: absolute; top: calc(100% + 6px); right: 0; min-width: 260px; background: var(--bg-surface, var(--ed-surface, #fff)); border: 1px solid var(--border-color, var(--ed-border, #ddd)); border-radius: 10px; padding: 0.35rem; box-shadow: var(--shadow-lg, 0 8px 24px rgba(0, 0, 0, 0.18)); z-index: 50; } .top-bar__menu-item { display: flex; align-items: flex-start; gap: 0.6rem; width: 100%; padding: 0.45rem 0.6rem; border-radius: 6px; border: none; background: none; cursor: pointer; color: var(--text-color, inherit); font: inherit; text-align: left; } .top-bar__menu-item:hover, .top-bar__menu-item:focus-visible { background: var(--code-bg, rgba(0, 0, 0, 0.05)); } .top-bar__menu-item svg { margin-top: 2px; color: var(--muted-color, #888); flex-shrink: 0; } .top-bar__menu-item-content { display: flex; flex-direction: column; min-width: 0; } .top-bar__menu-item-title { font-size: 0.85rem; font-weight: 500; color: var(--text-color); } .top-bar__menu-item-desc { font-size: 0.72rem; color: var(--muted-color); } .top-bar__menu-item--danger:hover, .top-bar__menu-item--danger:focus-visible { background: rgba(220, 53, 69, 0.1); } .top-bar__menu-item--danger svg, .top-bar__menu-item--danger .top-bar__menu-item-title { color: #dc3545; } /* ---- Chat panel (floating) ---- */ .chat-floating { position: fixed; bottom: 16px; left: 16px; width: 360px; height: 520px; max-height: calc(100vh - 80px); border-radius: 12px; overflow: hidden; display: flex; flex-direction: column; background: var(--ed-surface); border: 1px solid var(--ed-border); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); z-index: var(--z-overlay, 1300); } /* ---- Chat FAB ---- */ .chat-fab { position: fixed; bottom: 20px; left: 20px; width: 48px; height: 48px; border-radius: 50%; border: none; background: var(--primary-color); color: #000; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); z-index: var(--z-overlay, 1300); transition: background 0.15s; line-height: 0; } .chat-fab:hover { background: var(--primary-color-hover); } .chat-fab svg { width: 22px; height: 22px; } /* ---- Author form (rendered inside .ed-dialog) ---- */ .author-form__row { display: flex; gap: 8px; } .author-form__chips { display: flex; flex-wrap: wrap; gap: 4px; } dialog.ed-dialog.ed-dialog--author { max-width: 480px; } /* ---- Demo-only: agent live-rewrite highlight -------------------------- Two cooperating states visualize the AI agent's work on a selected paragraph during the recorded demo: 1. `.demo-agent-pending` - added by the demo script (showcase.ts) the moment the user selects a paragraph and asks the chat to rewrite it. Stays until the actual rewrite animation begins. Shows a "Agent is working..." badge so the viewer understands that although the chat is typing, the selected paragraph is what the agent is about to rewrite. 2. `.demo-agent-rewriting` - swapped in by the `__demo-chat` replace handler (App.tsx) while the paragraph is being retyped character-by-character. Same anchor styling as pending but the border gets a stronger pulse to sell "active write". Both classes share the base frame (border + tinted background + the floating badge) via the common `[class*="demo-agent-"]` selector. */ @keyframes demo-agent-pulse { 0% { box-shadow: 0 0 0 0 rgba(var(--primary-color-rgb, 14, 165, 233), 0.55); } 70% { box-shadow: 0 0 0 10px rgba(var(--primary-color-rgb, 14, 165, 233), 0); } 100% { box-shadow: 0 0 0 0 rgba(var(--primary-color-rgb, 14, 165, 233), 0); } } @keyframes demo-agent-badge-pulse { 0%, 100% { opacity: 0.85; transform: translateY(0); } 50% { opacity: 1; transform: translateY(-1px); } } @keyframes demo-agent-border-breathe { 0%, 100% { border-left-color: var(--primary-color); background: color-mix(in srgb, var(--primary-color) 8%, transparent); } 50% { border-left-color: color-mix(in srgb, var(--primary-color) 55%, white); background: color-mix(in srgb, var(--primary-color) 16%, transparent); } } .demo-agent-pending, .demo-agent-rewriting { position: relative; padding-left: 14px !important; border-left: 3px solid var(--primary-color); background: color-mix(in srgb, var(--primary-color) 10%, transparent); border-radius: 4px; transition: background 0.5s ease, border-color 0.5s ease; } /* Floating "Agent is working..." badge anchored to the top-right of the paragraph. Uses `::before` (and not `::after`) so it doesn't collide with other potential ProseMirror-injected decorations that live inside `::after`. */ .demo-agent-pending::before, .demo-agent-rewriting::before { content: "✦ Agent is working..."; position: absolute; top: -10px; right: 8px; font-size: 11px; font-weight: 600; color: var(--primary-color); background: var(--ed-surface, #fff); padding: 2px 10px; border-radius: 999px; border: 1px solid var(--primary-color); letter-spacing: 0.02em; white-space: nowrap; animation: demo-agent-badge-pulse 1.2s ease-in-out infinite; box-shadow: 0 0 12px color-mix(in srgb, var(--primary-color) 35%, transparent); z-index: 3; pointer-events: none; } /* Pending state: slow breathing so it says "queued / thinking" without competing with the character-by-character rewrite animation. */ .demo-agent-pending { animation: demo-agent-border-breathe 1.8s ease-in-out infinite; } /* Rewriting state: one sharp pulse at the start of the retype to emphasize the swap, then the standard tint stays until cleanup. */ .demo-agent-rewriting { animation: demo-agent-pulse 1.2s ease-out 1; } /* ---- Native selection recolored per user ---------------------------- Every native text selection anywhere in the app is painted with the current user's identity color (same hue as their cursor, agent focus and avatar ring). The color is fed via `--local-selection-bg`, set on `.editor-app` from `user.color` + `70` alpha. Using the native `::selection` pseudo-element (instead of a PM decoration) guarantees the highlight matches the full line-height of the text - exactly like a default browser selection, just in the user's color. */ .editor-app ::selection, .editor-app::selection, .editor-app *::selection { background-color: var(--local-selection-bg, #b4d5fe); color: inherit; } .editor-app ::-moz-selection, .editor-app::-moz-selection, .editor-app *::-moz-selection { background-color: var(--local-selection-bg, #b4d5fe); color: inherit; } /* While the chat is open the AgentFocus PM decoration owns the visual for the selected range (it's broadcast via awareness to the other peers, and it keeps the range visible even when the editor loses DOM focus to the chat textarea). We hide the native `::selection` *inside the editor only* to avoid stacking two tints when the editor still has focus. Elsewhere (sidebars, dialogs, chat) the recolored native selection keeps working normally. */ .editor-app--chat-open .ProseMirror ::selection, .editor-app--chat-open .ProseMirror::selection, .editor-app--chat-open .ProseMirror *::selection { background: transparent; color: inherit; } .editor-app--chat-open .ProseMirror ::-moz-selection, .editor-app--chat-open .ProseMirror::-moz-selection, .editor-app--chat-open .ProseMirror *::-moz-selection { background: transparent; color: inherit; } /* ---- Agent focus (shared across collaborators) ---------------------- Inline decoration injected by the AgentFocus extension when a user opens the AI chat with an active selection. The range is broadcast via Yjs awareness, and every peer - including self - renders the owner's highlight tinted with their color (inline style `background-color: ${color}70`). This is what keeps the range visible on the owner's screen after the editor loses DOM focus to the chat textarea. */ .agent-focus { transition: background-color 150ms ease-out; border-radius: 2px; } /* Floating label anchored at the start of the range, mirroring the collaboration cursor label pattern. The anchor is a zero-width inline span that provides a positioning context for the label. */ /* Zero-impact inline anchor, positioned relative so the absolute label hangs from it. Using plain `display: inline` (not `inline-block`) keeps the anchor on the same baseline as the surrounding text so `top: -1.4em` on the label lands where a cursor label would. */ .agent-focus__anchor { position: relative; display: inline; pointer-events: none; } /* Match `.collaboration-cursor__label` so an agent badge reads the same as a real collaborator's cursor label - only the "'s agent" suffix tells them apart. */ .agent-focus__label { position: absolute; top: -1.4em; left: -1px; display: inline-flex; align-items: center; gap: 3px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; font-size: 0.65rem; font-weight: 600; padding: 0.1rem 0.35rem; border-radius: 3px 3px 3px 0; white-space: nowrap; color: #000; user-select: none; pointer-events: none; z-index: 2; } .agent-focus__avatar { width: 14px; height: 14px; border-radius: 50%; object-fit: cover; flex-shrink: 0; } .agent-focus__text { line-height: 1; } /* ---- Settings drawer ---- */ .settings-drawer { padding: 20px; display: flex; flex-direction: column; height: 100%; } .settings-drawer__header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; } .settings-drawer__title { font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--ed-text-secondary); font-size: 0.75rem; } .settings-drawer__body { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 20px; } /* ---- Chat panel ---- */ .chat-panel { display: flex; flex-direction: column; flex: 1; min-height: 0; width: 100%; } .chat-panel__header { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; border-bottom: 1px solid var(--ed-border); flex-shrink: 0; gap: 8px; } .chat-panel__header-right { display: flex; align-items: center; gap: 6px; margin-left: auto; } .chat-panel__label { font-weight: 600; color: var(--ed-text); font-size: 0.75rem; white-space: nowrap; } .icon-btn--sm { padding: 3px; } .chat-panel__messages { flex: 1; overflow: auto; padding: 8px 12px; display: flex; flex-direction: column; gap: 12px; } .chat-panel__empty { display: flex; align-items: center; justify-content: center; flex: 1; opacity: 0.4; text-align: center; font-size: 0.75rem; max-width: 200px; margin: 0 auto; } .chat-panel__thinking { display: flex; align-items: center; gap: 8px; padding: 0 8px; font-size: 0.75rem; } .shimmer-text { background: linear-gradient( 90deg, var(--ed-text-disabled) 0%, var(--ed-text-disabled) 40%, var(--text-color) 50%, var(--ed-text-disabled) 60%, var(--ed-text-disabled) 100% ); background-size: 200% 100%; -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; animation: shimmer 2s ease-in-out infinite; } @keyframes shimmer { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } } .chat-panel__error { padding: 4px 8px; background: rgba(244, 67, 54, 0.15); border-radius: 4px; font-size: 0.75rem; color: var(--ed-error); } .chat-panel__input { padding: 8px 12px; border-top: 1px solid var(--ed-border); flex-shrink: 0; } .chat-panel__model-select { font-size: 0.65rem; padding: 2px 20px 2px 6px; background-color: transparent; border: 1px solid var(--ed-border); border-radius: 4px; color: var(--ed-text-secondary); max-width: 140px; } .chat-panel__input-row { display: flex; gap: 4px; align-items: flex-end; } .chat-panel__textarea { flex: 1; background: transparent; border: none; color: var(--ed-text); font-size: 0.85rem; font-family: inherit; line-height: 1.5; padding: 4px 0; resize: none; outline: none; } .chat-panel__textarea::placeholder { color: var(--ed-text-disabled); } /* ---- Chat bubbles ---- */ .chat-bubble { display: flex; flex-direction: column; gap: 4px; } .chat-bubble__text { max-width: 95%; padding: 6px 12px; border-radius: 8px; font-size: 0.8rem; line-height: 1.6; color: var(--ed-text-secondary); word-break: break-word; } .chat-bubble__text--user { white-space: pre-wrap; } /* Markdown inside assistant bubbles */ .chat-bubble__text p { margin: 0 0 0.4em; } .chat-bubble__text p:last-child { margin-bottom: 0; } .chat-bubble__text ul, .chat-bubble__text ol { margin: 0.3em 0; padding-left: 1.4em; } .chat-bubble__text li { margin-bottom: 0.15em; } .chat-bubble__text code { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 0.85em; background: var(--ed-surface); padding: 1px 4px; border-radius: 3px; } .chat-bubble__text pre { background: var(--ed-surface); padding: 8px 10px; border-radius: 6px; overflow-x: auto; margin: 0.4em 0; } .chat-bubble__text pre code { background: none; padding: 0; } .chat-bubble__text strong { color: var(--ed-text); } .chat-bubble__text a { color: var(--primary-color); text-decoration: underline; } .chat-bubble__text table { border-collapse: collapse; font-size: 0.85em; margin: 0.4em 0; } .chat-bubble__text th, .chat-bubble__text td { border: 1px solid var(--ed-border); padding: 3px 6px; } .chat-bubble__text blockquote { margin: 0.4em 0; padding-left: 10px; border-left: 2px solid var(--ed-border); color: var(--ed-text-disabled); } .chat-bubble__text h1, .chat-bubble__text h2, .chat-bubble__text h3, .chat-bubble__text h4 { margin: 0.6em 0 0.2em; font-size: 0.9em; color: var(--ed-text); } .chat-bubble__text--user { align-self: flex-end; background: rgba(149, 141, 241, 0.15); color: var(--ed-text); } /* Spin animation for Loader icon */ .spin { animation: spin 1.2s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .chat-bubble__tool { display: flex; align-items: center; gap: 4px; padding: 0 12px; opacity: 0.6; font-size: 0.65rem; color: var(--ed-text-secondary); } /* ---- Sync indicator ---- */ .sync-indicator { display: inline-flex; align-items: center; gap: 5px; padding: 3px 8px; border-radius: 6px; font-size: 0.7rem; font-weight: 500; transition: opacity 0.2s, color 0.2s; cursor: default; user-select: none; } .sync-indicator__label { white-space: nowrap; } .sync-indicator--saved { color: var(--ed-text-disabled); opacity: 0.7; } .sync-indicator--pending { color: var(--ed-accent, #958df1); opacity: 1; } .sync-indicator--offline { color: #e15759; opacity: 1; } /* `error` is the loudest state - bright red, slight bg tint, and * a pulsing dot to scream "your data isn't safe". The other * states stay flat to avoid attention-seeking noise. */ .sync-indicator--error { color: #e15759; background: color-mix(in srgb, #e15759 12%, transparent); opacity: 1; animation: sync-indicator-pulse 2s ease-in-out infinite; } @keyframes sync-indicator-pulse { 0%, 100% { background: color-mix(in srgb, #e15759 12%, transparent); } 50% { background: color-mix(in srgb, #e15759 22%, transparent); } } .sync-indicator .spin { animation: sync-spin 1s linear infinite; } @keyframes sync-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }