diff --git a/frontend/src/assets/editor-shared.css b/frontend/src/assets/editor-shared.css index 50cd8dc..9637035 100644 --- a/frontend/src/assets/editor-shared.css +++ b/frontend/src/assets/editor-shared.css @@ -15,18 +15,6 @@ padding: 1rem 1.5rem 0.5rem; border-bottom: 1px solid var(--fs-border-color); } -.editor-body { - flex: 1; - min-height: 0; - display: flex; - overflow: hidden; -} -.editor-main { - flex: 1; - min-width: 0; - overflow-y: auto; - padding: 0.75rem 1.5rem 1.5rem; -} /* ── Toolbar & inputs ── */ .toolbar { @@ -106,95 +94,6 @@ .tag-check { font-size: 0.7rem; } - -/* ── Assist panel ── */ -.assist-panel { - width: 320px; - flex-shrink: 0; - border-left: 1px solid var(--fs-border-color); - background: var(--fs-surface-raised); - display: flex; - flex-direction: column; - overflow: hidden; -} -.assist-panel-header { - flex-shrink: 0; - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.65rem 0.9rem; - border-bottom: 1px solid var(--fs-border-color); -} -.assist-panel-title { - flex: 1; - font-size: 0.8rem; - font-weight: 500; - color: var(--fs-text-secondary); - text-transform: uppercase; - letter-spacing: 0.05em; -} -.assist-panel-body { - flex: 1; - min-height: 0; - overflow-y: auto; - padding: 0.75rem 0.9rem 1rem; - display: flex; - flex-direction: column; - gap: 0.6rem; -} - -/* Section list */ -.assist-sections-label { - font-size: 0.72rem; - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.04em; - color: var(--fs-text-tertiary); - margin-bottom: 0.2rem; -} -.assist-sections { - border: 1px solid var(--fs-border-color); - border-radius: var(--fs-radius-sm); - background: var(--fs-surface-page); - max-height: 200px; - overflow-y: auto; - flex-shrink: 0; -} -.assist-section-item { - padding: 0.35rem 0.7rem; - cursor: pointer; - font-size: 0.82rem; - border-left: 3px solid transparent; - color: var(--fs-text-primary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.assist-section-item:hover { - background: var(--fs-surface-raised); -} -.assist-section-item.selected { - border-left-color: var(--fs-accent); - background: var(--fs-surface-raised); - font-weight: 500; -} -.assist-empty, -.assist-hint { - padding: 0.6rem 0.7rem; - font-size: 0.82rem; - color: var(--fs-text-tertiary); -} -.assist-target-preview { - font-size: 0.8rem; - color: var(--fs-text-secondary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.assist-target-preview em { - font-style: normal; - color: var(--fs-text-primary); -} .assist-instruction { width: 100%; padding: 0.5rem 0.65rem; @@ -213,33 +112,6 @@ gap: 0.5rem; } -/* Streaming */ -.assist-streaming-label { - font-size: 0.8rem; - color: var(--fs-text-secondary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.assist-preview-box { - padding: 0.65rem; - border: 1px solid var(--fs-border-color); - border-radius: var(--fs-radius-sm); - background: var(--fs-surface-page); - font-size: 0.9rem; - max-height: 300px; - overflow-y: auto; -} -.typing-indicator { - color: var(--fs-text-tertiary); - font-size: 0.75rem; - letter-spacing: 0.15em; - animation: blink 1s step-end infinite; -} -@keyframes blink { - 50% { opacity: 0; } -} - /* Active hint shown in the panel while output is inline */ .assist-active-hint { padding: 0.5rem 0.75rem; @@ -257,16 +129,6 @@ font-size: 0.85rem; color: var(--fs-error); } - -/* Review / diff */ -.assist-review-header { - display: flex; - align-items: center; - justify-content: space-between; - font-size: 0.8rem; - font-weight: 500; - color: var(--fs-text-secondary); -} .diff-view { border: 1px solid var(--fs-border-color); border-radius: var(--fs-radius-sm); @@ -398,22 +260,9 @@ /* ── Mobile ── */ @media (max-width: 768px) { - .editor-body { - flex-direction: column; - } - .assist-panel { - width: auto; - flex: 0 0 45%; - border-left: none; - border-top: 1px solid var(--fs-border-color); - border-radius: var(--fs-radius-lg) var(--fs-radius-lg) 0 0; - } .editor-header { padding: 0.75rem 1rem 0.5rem; } - .editor-main { - padding: 0.5rem 1rem 1rem; - } } /* --------------------------------------------------------------------------- diff --git a/frontend/src/assets/theme.css b/frontend/src/assets/theme.css index 38467d2..3a000e9 100644 --- a/frontend/src/assets/theme.css +++ b/frontend/src/assets/theme.css @@ -272,17 +272,10 @@ button:not(:disabled):active, display: none !important; } button, - [role="button"], - .btn-new-conv, - .btn-send { + [role="button"] { min-height: 44px; } } -@media (min-width: 769px) { - .hide-desktop { - display: none !important; - } -} /* Neutral hairline scrollbars — chrome is structural, not branded */ ::-webkit-scrollbar { diff --git a/frontend/src/components/AppHeader.vue b/frontend/src/components/AppHeader.vue index 3066255..f12f472 100644 --- a/frontend/src/components/AppHeader.vue +++ b/frontend/src/components/AppHeader.vue @@ -212,43 +212,6 @@ router.afterEach(() => { box-shadow: 0 0 16px color-mix(in srgb, var(--fs-accent) 30%, transparent); } -/* Status indicator */ -.status-indicator { - display: flex; - align-items: center; - gap: 0.3rem; - cursor: default; - padding: 0 0.25rem; -} -.status-dot { - width: 8px; - height: 8px; - border-radius: 50%; - flex-shrink: 0; -} -.status-text { - font-size: 0.75rem; - font-weight: 500; - color: var(--fs-text-tertiary); -} -/* Status dots are indicator lights, not semantic-palette buttons — - they want to read as vital (Moss/Warning/Error are too muted for - a "ready" indicator). Hardcoded bright values; the rest of the - system still uses the semantic tokens. */ -.status-green .status-dot { background: #4ade80; animation: status-pulse 2.5s ease-in-out infinite; } -.status-yellow .status-dot { background: #facc15; animation: pulse-dot 2s infinite; } -.status-orange .status-dot { background: #f97316; } -.status-red .status-dot { background: #ef4444; } -.status-gray .status-dot { background: var(--fs-text-tertiary); animation: pulse-dot 2s infinite; } -@keyframes pulse-dot { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.3; } -} -@keyframes status-pulse { - 0%, 100% { box-shadow: 0 0 4px rgba(74, 222, 128, 0.4); } - 50% { box-shadow: 0 0 10px rgba(74, 222, 128, 0.6); } -} - /* Icon buttons (?, theme, gear) */ .btn-icon { background: none; @@ -263,8 +226,7 @@ router.afterEach(() => { align-items: center; justify-content: center; } -.btn-icon:hover, -.btn-icon.active { +.btn-icon:hover { background: var(--fs-surface-raised); color: var(--fs-text-primary); border-color: var(--fs-accent); @@ -382,7 +344,6 @@ router.afterEach(() => { .nav-center { display: none; } - .status-indicator, .btn-icon, .user-info { display: none; diff --git a/frontend/src/components/ShareDialog.vue b/frontend/src/components/ShareDialog.vue index e008264..594b35c 100644 --- a/frontend/src/components/ShareDialog.vue +++ b/frontend/src/components/ShareDialog.vue @@ -231,7 +231,6 @@ onMounted(async () => { color: var(--fs-text-primary); } - .share-tabs { display: flex; gap: 0.25rem; @@ -307,7 +306,6 @@ onMounted(async () => { .user-result-item:hover { background: var(--fs-surface-raised); } .user-result-name { font-weight: 600; font-size: 0.88rem; } -.user-result-email { color: var(--fs-text-tertiary); font-size: 0.8rem; } .perm-select { padding: 0.45rem 0.5rem; diff --git a/frontend/src/components/WorkspaceNoteEditor.vue b/frontend/src/components/WorkspaceNoteEditor.vue index cb4c96d..d54fb55 100644 --- a/frontend/src/components/WorkspaceNoteEditor.vue +++ b/frontend/src/components/WorkspaceNoteEditor.vue @@ -471,7 +471,6 @@ defineExpose({ reload: loadProjectNotes }); flex: 1; } - .rail-search-input { flex: 1; background: transparent; @@ -575,8 +574,6 @@ defineExpose({ reload: loadProjectNotes }); align-items: center; } -.note-row:hover .btn-delete { opacity: 1; } - /* Editor UI */ .panel-header { display: flex; @@ -624,8 +621,6 @@ defineExpose({ reload: loadProjectNotes }); } .tag-row > :first-child { flex: 1; min-width: 0; } -.btn-suggest-tags { flex-shrink: 0; align-self: center; } - .tag-suggestions { display: flex; flex-wrap: wrap; @@ -653,7 +648,6 @@ defineExpose({ reload: loadProjectNotes }); color: var(--fs-accent); } - .link-suggest-strip { display: flex; align-items: center; diff --git a/frontend/src/views/KnowledgeView.vue b/frontend/src/views/KnowledgeView.vue index 73a09d1..e555efd 100644 --- a/frontend/src/views/KnowledgeView.vue +++ b/frontend/src/views/KnowledgeView.vue @@ -607,14 +607,6 @@ onUnmounted(() => { text-decoration: none; font-size: 0.78rem; } -.today-link { - color: var(--fs-accent); - text-decoration: none; - font-weight: 500; - opacity: 0.85; - transition: opacity 0.15s; -} -.today-link:hover { opacity: 1; } /* ── Main layout ─────────────────────────────────────────── */ .knowledge-layout { @@ -1042,7 +1034,6 @@ onUnmounted(() => { height: 100%; } - /* A set someone already ruled on — quiet, not celebratory: it means "skip". */ .dup-claimed { font-size: 0.72rem; diff --git a/frontend/src/views/ProjectListView.vue b/frontend/src/views/ProjectListView.vue index 841bace..d373e2a 100644 --- a/frontend/src/views/ProjectListView.vue +++ b/frontend/src/views/ProjectListView.vue @@ -366,12 +366,6 @@ function overallPct(project: Project): { total: number; pct: number } { border-bottom-color: var(--fs-accent); font-weight: 500; } - -.loading-msg { - color: var(--fs-text-tertiary); - font-size: 0.9rem; - margin-top: 1rem; -} .error-msg { margin-top: 1rem; } diff --git a/frontend/src/views/ProjectView.vue b/frontend/src/views/ProjectView.vue index ad68a17..5402a25 100644 --- a/frontend/src/views/ProjectView.vue +++ b/frontend/src/views/ProjectView.vue @@ -1444,7 +1444,8 @@ async function confirmDelete() { } /* The input itself is the .fs-input canon (components.css); only the layout remainder lives here. */ -.edit-input, .edit-textarea, .edit-select { box-sizing: border-box; width: 100%; } +.edit-textarea, +.edit-select { box-sizing: border-box; width: 100%; } .edit-textarea { resize: vertical; } /* Save panel: Moss action-primary per Hybrid rule */ diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index 57e08ca..890a087 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -2645,70 +2645,6 @@ async function deleteUser(userId: number) { line-height: 1.5; } -/* ── Model Management ───────────────────────────────────────── */ -.model-mgmt-header { - display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; margin-bottom: 0.75rem; -} -.model-mgmt-header h2 { margin: 0; } -.model-mgmt-header .section-desc { margin: 0.25rem 0 0; } -.model-list { display: flex; flex-direction: column; gap: 0.25rem; margin-bottom: 0.75rem; } -.model-row { - display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; - padding: 0.45rem 0.6rem; - background: var(--fs-surface-raised); - border: 1px solid var(--fs-border-color); - border-radius: 6px; -} -.model-row-info { display: flex; align-items: center; gap: 0.4rem; min-width: 0; flex: 1; } -.model-name { font-size: 0.88rem; font-weight: 500; font-family: monospace; } -.model-badge { - font-size: 0.68rem; padding: 0.1rem 0.4rem; border-radius: 3px; font-weight: 500; white-space: nowrap; -} -.model-badge--loaded { background: color-mix(in srgb, #22c55e 18%, transparent); color: #22c55e; } -.model-badge--default { background: color-mix(in srgb, var(--fs-accent) 18%, transparent); color: var(--fs-accent); } -.model-row-right { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; } -.model-size { font-size: 0.78rem; color: var(--fs-text-tertiary); } -.model-delete-btn { - background: none; border: none; cursor: pointer; color: var(--fs-text-tertiary); - font-size: 0.8rem; padding: 0.2rem 0.35rem; border-radius: 3px; line-height: 1; - transition: color 0.15s, background 0.15s; -} -.model-delete-btn:hover:not(:disabled) { color: var(--fs-action-destructive); background: color-mix(in srgb, var(--fs-action-destructive) 10%, transparent); } -.model-delete-btn:disabled { opacity: 0.4; cursor: default; } -.model-pull-form { display: flex; gap: 0.5rem; margin-top: 0.5rem; } -.model-pull-form .input { flex: 1; } -.model-suggestions { display: flex; align-items: center; gap: 0.35rem; flex-wrap: wrap; margin-top: 0.4rem; } -.suggestions-label { font-size: 0.75rem; color: var(--fs-text-tertiary); white-space: nowrap; } -.suggestion-chip { - font-size: 0.72rem; padding: 0.15rem 0.5rem; border-radius: 4px; - border: 1px solid var(--fs-border-color); background: var(--fs-surface-raised); - cursor: pointer; font-family: monospace; color: var(--fs-text-primary); - transition: border-color 0.12s, background 0.12s; -} -.suggestion-chip:hover:not(:disabled) { border-color: var(--fs-accent); background: color-mix(in srgb, var(--fs-accent) 8%, var(--fs-surface-raised)); } -.suggestion-chip:disabled { opacity: 0.4; cursor: default; } -.model-pull-progress { margin-top: 0.6rem; } -.pull-status { font-size: 0.8rem; color: var(--fs-text-tertiary); margin-bottom: 0.25rem; } -.pull-bar-track { - height: 4px; background: var(--fs-border-color); border-radius: 2px; overflow: hidden; -} -.pull-bar-fill { - height: 100%; background: var(--fs-accent); border-radius: 2px; - transition: width 0.3s ease; -} -.pull-bar-indeterminate { - height: 4px; background: var(--fs-border-color); border-radius: 2px; - position: relative; overflow: hidden; -} -.pull-bar-indeterminate::after { - content: ""; position: absolute; top: 0; left: -40%; - width: 40%; height: 100%; background: var(--fs-accent); border-radius: 2px; - animation: indeterminate 1.2s ease-in-out infinite; -} -@keyframes indeterminate { - 0% { left: -40%; } 100% { left: 100%; } -} - /* Assistant — 2-col internal grid */ .assistant-grid { display: grid; @@ -2736,11 +2672,6 @@ async function deleteUser(userId: number) { width: 100%; box-sizing: border-box; } -.field-hint-warn { - display: block; - margin-top: 0.3rem; - color: #f59e0b; -} .actions { display: flex; align-items: center; @@ -2809,16 +2740,6 @@ async function deleteUser(userId: number) { color: var(--fs-error); } -.retention-row { - display: flex; - align-items: center; - gap: 0.5rem; - margin-top: 0.4rem; -} -.retention-input { - width: 6rem; -} - /* Data buttons */ .data-actions { display: flex; @@ -2945,42 +2866,6 @@ async function deleteUser(userId: number) { max-width: 480px; } -/* Push notifications */ -.push-unsupported { - font-size: 0.875rem; - color: var(--fs-text-tertiary); -} -.push-status-row { - display: flex; - align-items: center; - gap: 0.5rem; - margin-bottom: 0.4rem; - font-size: 0.875rem; -} -.push-status-label { - color: var(--fs-text-secondary); - font-weight: 500; -} -.push-permission-badge, -.push-sub-badge { - font-size: 0.72rem; - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.04em; - padding: 0.1rem 0.4rem; - border-radius: 999px; - background: color-mix(in srgb, var(--fs-text-tertiary) 15%, transparent); - color: var(--fs-text-tertiary); -} -.perm-granted { background: color-mix(in srgb, var(--fs-success) 15%, transparent); color: var(--fs-success); } -.perm-denied { background: color-mix(in srgb, var(--fs-error) 15%, transparent); color: var(--fs-error); } -.sub-active { background: color-mix(in srgb, var(--fs-success) 15%, transparent); color: var(--fs-success); } -.push-error { - font-size: 0.82rem; - color: var(--fs-error); - margin: 0.25rem 0 0; -} - @media (max-width: 768px) { .settings-root { flex-direction: column; @@ -3175,25 +3060,6 @@ async function deleteUser(userId: number) { margin-right: 0.25rem; } -/* ── About / version ─────────────────────────────────────────── */ -.version-line { - margin: 0; - font-size: 0.9rem; - color: var(--fs-text-primary); - display: flex; - align-items: center; - gap: 0.6rem; -} -.version-badge { - font-family: ui-monospace, monospace; - font-size: 0.8rem; - padding: 0.15rem 0.5rem; - background: var(--fs-surface-raised); - border: 1px solid var(--fs-accent); - border-radius: 4px; - color: var(--fs-accent); -} - /* ── Groups tab ──────────────────────────────────────────────── */ /* Moss action-primary per Hybrid */ @@ -3278,7 +3144,6 @@ async function deleteUser(userId: number) { flex-shrink: 0; } - .member-search-wrap { flex: 1; position: relative; @@ -3311,7 +3176,6 @@ async function deleteUser(userId: number) { } .member-result-item:hover { background: var(--fs-surface-hover); } .member-result-name { font-weight: 500; font-size: 0.85rem; } -.member-result-email { color: var(--fs-text-tertiary); font-size: 0.78rem; } .role-select { padding: 0.4rem 0.5rem; @@ -3361,76 +3225,6 @@ async function deleteUser(userId: number) { padding: 0.25rem 0.5rem; } -/* Profile — Locations + Journal sections */ -.location-row { - display: flex; - gap: 0.5rem; - margin-top: 0.25rem; -} -.geo-msg { - font-size: 0.78rem; - margin: 0.2rem 0 0; -} -.geo-ok { color: var(--fs-success); } -.geo-error { color: var(--fs-error); } -.geo-pending { color: var(--fs-text-tertiary); } - -.unit-toggle { - display: flex; - gap: 0; - border: 1px solid var(--fs-border-color); - border-radius: var(--fs-radius-sm); - overflow: hidden; - width: fit-content; - margin-top: 0.25rem; -} -.unit-btn { - padding: 0.4rem 1rem; - background: var(--fs-surface-raised); - color: var(--fs-text-tertiary); - font-size: 0.85rem; - cursor: pointer; - border: none; - font-family: inherit; - transition: background 0.15s, color 0.15s; -} -.unit-btn:not(:last-child) { - border-right: 1px solid var(--fs-border-color); -} -.unit-btn.active { - background: var(--fs-action-primary); - color: var(--fs-text-on-action); -} -.unit-btn:hover:not(.active) { - color: var(--fs-text-primary); -} - -.checkbox-label { - display: flex; - align-items: center; - gap: 0.5rem; - cursor: pointer; - font-size: 0.9rem; -} -.checkbox-label input[type="checkbox"] { - cursor: pointer; -} - -.time-row { - display: flex; - align-items: center; - gap: 0.4rem; -} -.time-input { - width: 4rem; - text-align: center; -} -.time-sep { - font-size: 1.1rem; - color: var(--fs-text-tertiary); -} -.time-sep--quiet { font-size: 0.9rem; } - /* API Keys tab */ .api-key-create-form { display: flex; @@ -3493,24 +3287,6 @@ async function deleteUser(userId: number) { .scope-badge.write { background: color-mix(in srgb, #10b981 15%, transparent); color: #10b981; } .settings-empty { opacity: 0.5; margin-top: 1rem; } .settings-description { opacity: 0.7; margin-bottom: 1rem; line-height: 1.5; } - -/* Scribe MCP section */ -.mcp-status { opacity: 0.6; font-size: 0.9rem; } -.mcp-unavailable p { opacity: 0.7; } -.mcp-available { display: flex; flex-direction: column; gap: 1.25rem; } -.mcp-pkg-row { - display: flex; - align-items: center; - gap: 1rem; - padding: 0.65rem 0.9rem; - background: color-mix(in srgb, var(--fs-accent) 8%, transparent); - border: 1px solid color-mix(in srgb, var(--fs-accent) 25%, transparent); - border-radius: 8px; -} -.mcp-pkg-name { font-family: monospace; font-size: 0.9rem; flex: 1; } -.mcp-install-steps h3 { font-size: 0.95rem; font-weight: 500; margin-bottom: 0.75rem; } -.mcp-install-steps ol { padding-left: 1.25rem; display: flex; flex-direction: column; gap: 0.75rem; } -.mcp-install-steps li { line-height: 1.6; font-size: 0.9rem; } .mcp-code { margin-top: 0.4rem; padding: 0.55rem 0.75rem; @@ -3599,207 +3375,6 @@ async function deleteUser(userId: number) { opacity: 0.7; line-height: 1.5; } -.mcp-other p { font-size: 0.9rem; line-height: 1.6; } -.mcp-plain-list { - list-style: disc; - padding-left: 1.25rem; - display: flex; - flex-direction: column; - gap: 0.5rem; - font-size: 0.9rem; - line-height: 1.6; -} - -/* Voice tab */ -.voice-status-row { - display: flex; - align-items: center; - gap: 0.5rem; - flex-wrap: wrap; - margin-top: 0.5rem; -} - -/* Voice Library (admin) */ -.voice-library-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; -} -.voice-library-toolbar { - display: flex; - gap: 0.5rem; - align-items: center; - margin: 1rem 0; -} -.voice-library-toolbar .input { - flex: 1; -} -.voice-library-list { - list-style: none; - margin: 0; - padding: 0; - border-top: 1px solid var(--fs-border-color); - max-height: 480px; - overflow-y: auto; -} -.voice-library-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 0.65rem 0.25rem; - border-bottom: 1px solid var(--fs-border-color); -} -.voice-library-meta { - min-width: 0; - flex: 1; -} -.voice-library-id { - font-family: var(--fs-font-mono); - font-size: 0.9rem; - font-weight: 500; -} -.voice-library-sub { - color: var(--fs-text-tertiary); - font-size: 0.8rem; - margin-top: 0.15rem; -} -.voice-library-actions { - display: flex; - align-items: center; - gap: 0.5rem; - flex-shrink: 0; -} -.voice-library-empty { - padding: 1rem 0; - color: var(--fs-text-tertiary); - text-align: center; - font-style: italic; -} -.voice-badge { - font-size: 0.7rem; - font-weight: 500; - padding: 0.15rem 0.5rem; - border-radius: 999px; - text-transform: uppercase; - letter-spacing: 0.04em; -} -.voice-badge--bundled { - background: color-mix(in srgb, var(--fs-text-tertiary) 10%, transparent); - color: var(--fs-text-tertiary); - border: 1px solid var(--fs-border-color); -} -.status-badge { - font-size: 0.75rem; - font-weight: 500; - padding: 0.15rem 0.55rem; - border-radius: 999px; -} -.status-on { - background: color-mix(in srgb, var(--fs-success) 15%, transparent); - color: var(--fs-success); - border: 1px solid color-mix(in srgb, var(--fs-success) 40%, transparent); -} -.status-off { - background: color-mix(in srgb, var(--fs-text-tertiary) 10%, transparent); - color: var(--fs-text-tertiary); - border: 1px solid var(--fs-border-color); -} -.radio-group { - display: flex; - flex-direction: column; - gap: 0.65rem; - margin-top: 0.35rem; -} -.radio-option { - display: flex; - align-items: flex-start; - gap: 0.55rem; - cursor: pointer; -} -.radio-option input[type="radio"] { margin-top: 0.2rem; flex-shrink: 0; } -.radio-option span { display: flex; flex-direction: column; gap: 0.15rem; } -.radio-option strong { font-size: 0.875rem; } -.range-input { - width: 100%; - max-width: 360px; - accent-color: var(--fs-accent); - margin: 0.35rem 0 0.2rem; -} -.range-labels { - display: flex; - justify-content: space-between; - max-width: 360px; - font-size: 0.75rem; - color: var(--fs-text-tertiary); -} -.form-actions { - margin-top: 1rem; -} - -.voice-admin-spinner { - display: inline-flex; - gap: 3px; - align-items: center; - margin-left: 0.4rem; - vertical-align: middle; -} -.voice-admin-spinner span { - display: inline-block; - width: 4px; - height: 4px; - border-radius: 50%; - background: var(--fs-text-tertiary); - animation: va-dot-bounce 1.2s ease-in-out infinite; -} -.voice-admin-spinner span:nth-child(2) { animation-delay: 0.2s; } -.voice-admin-spinner span:nth-child(3) { animation-delay: 0.4s; } - -/* ── Voice blend ────────────────────────────────────────────────────────── */ -.blend-slots { - display: flex; - flex-direction: column; - gap: 0.75rem; - margin-bottom: 0.5rem; -} -.blend-slot { - background: color-mix(in srgb, var(--fs-surface-hover) 60%, transparent); - border: 1px solid var(--fs-border-color); - border-radius: var(--fs-radius-lg); - padding: 0.75rem 1rem; - display: flex; - flex-direction: column; - gap: 0.5rem; -} -.blend-voice-select { - width: 100%; -} -.blend-weight-row { - display: flex; - align-items: center; - gap: 0.75rem; -} -.blend-weight-slider { - flex: 1; -} -.blend-weight-label { - font-size: 0.85rem; - font-variant-numeric: tabular-nums; - min-width: 2.5rem; - text-align: right; - color: var(--fs-text-secondary); -} -.blend-actions { - margin-top: 0.25rem; -} -.toggle-label { - display: flex; - align-items: center; - gap: 0.5rem; - cursor: pointer; - font-size: 0.9rem; -} /* ── Profile tab ─────────────────────────────────────────────────────────── */ .day-picker { @@ -3826,21 +3401,4 @@ async function deleteUser(userId: number) { color: var(--fs-accent); font-weight: 500; } -.learned-summary { - background: var(--fs-surface-raised); - border: 1px solid var(--fs-border-color); - border-left: 3px solid var(--fs-accent); - border-radius: 8px; - padding: 0.85rem 1rem; - font-size: 0.9rem; - line-height: 1.55; - color: var(--fs-text-primary); - white-space: pre-wrap; - margin-bottom: 0.75rem; -} -.learned-empty { - font-size: 0.85rem; - color: var(--fs-text-tertiary); - margin-bottom: 0.75rem; -} diff --git a/frontend/src/views/SnippetEditorView.vue b/frontend/src/views/SnippetEditorView.vue index 07df8d4..c535302 100644 --- a/frontend/src/views/SnippetEditorView.vue +++ b/frontend/src/views/SnippetEditorView.vue @@ -383,7 +383,6 @@ function cancel() { color: var(--fs-accent); } - .form { display: flex; flex-direction: column; @@ -400,9 +399,6 @@ function cancel() { grid-template-columns: 1fr 1fr; gap: 1rem; } -.field-row.three { - grid-template-columns: 1fr 1.4fr 1fr; -} .field label, .location-set legend { font-size: 0.8rem; @@ -552,8 +548,7 @@ function cancel() { } @media (max-width: 600px) { - .field-row, - .field-row.three { + .field-row { grid-template-columns: 1fr; } } diff --git a/frontend/src/views/TaskEditorView.vue b/frontend/src/views/TaskEditorView.vue index 33796ef..f19115f 100644 --- a/frontend/src/views/TaskEditorView.vue +++ b/frontend/src/views/TaskEditorView.vue @@ -803,7 +803,8 @@ useEditorGuards(dirty, save); max-width: 1600px; } -/* Replace .editor-body for task editor */ +/* The task editor's own body row. It began as a replacement for the shared + .editor-body, which nothing used afterwards and has since been deleted. */ .task-body { flex: 1; min-height: 0; diff --git a/plugin/.claude-plugin/plugin.json b/plugin/.claude-plugin/plugin.json index f8ac872..794ecf9 100644 --- a/plugin/.claude-plugin/plugin.json +++ b/plugin/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "scribe", "description": "Scribe system-of-record for Claude Code: MCP tools over your notes/tasks/projects/rules, a session-start push channel that surfaces your always-on rules + active-project context, process-skills (writing-plans, systematic-debugging, verification, brainstorming, reusing-code), and your saved Scribe Processes auto-surfaced as skills (/scribe:sync). Replaces superpowers + file-memory with one app-backed plugin.", - "version": "0.1.44", + "version": "0.1.45", "author": { "name": "Bryan Van Deusen" }, "mcpServers": { "scribe": { diff --git a/plugin/skills/shape-accounting/SKILL.md b/plugin/skills/shape-accounting/SKILL.md index e9196a3..23a2ccf 100644 --- a/plugin/skills/shape-accounting/SKILL.md +++ b/plugin/skills/shape-accounting/SKILL.md @@ -125,7 +125,11 @@ the last sweep left it. Three surfaces say so without anyone running an audit templates, one recipe → derive; one template each, different purposes → dismiss. `list_shapes(flag="unused-css")` is the map's negative space — css rules no template names, a deletion candidate to look at, never - auto-deleted (a class built at runtime is invisible to the map). + auto-deleted. The map reads the two class forms templates don't spell out + — a ``'s generated classes, and the prefix of a + concatenated name (`` `status-${s}` `` credits every `status-…` rule) — + so what it flags is worth reading. What it still cannot see is a name + assembled in a script (`classList.add`), so confirm before deleting. After the one-time pay-down the derive queue reads empty; anything in it afterwards is drift of the moment, and the hint already said so at the write. diff --git a/src/scribe/mcp/tools/shapes.py b/src/scribe/mcp/tools/shapes.py index 3ba6e6d..c1d5dd3 100644 --- a/src/scribe/mcp/tools/shapes.py +++ b/src/scribe/mcp/tools/shapes.py @@ -118,8 +118,9 @@ async def list_shapes( whose body changed since judged (the judgment stands; confirm it again with classify_shapes, or re-judge); "unused-css" (milestone 302): live css rules no file's markup names — a - deletion candidate to look at, never auto-deleted (the map reads - templates only; a class built at runtime is invisible to it). + deletion candidate to look at, never auto-deleted. Transition + classes and concatenated names are read (#2970), so the list is + worth acting on; a name assembled in a script still is not. Returns {"shapes": [...], "total": N} — total counts every match, not just this page. Every css row carries `used_by` {count, paths} — the @@ -305,8 +306,9 @@ async def refresh_pattern_coverage(project_id: int) -> dict: unclassified, repos, largest_gaps, `proposed` (canon proposals awaiting confirmation), `derive_groups` (the biggest repeats-with-no-canon families, each css one with `consumers` — the files whose markup - render it, milestone 302), `unused_css` (css rules no template names; - None where the map has no evidence of templates), `derive_new` (copies + render it, milestone 302), `unused_css` (css rules no template names — + counting a ``'s generated classes and concatenated + names as named, #2970; None where the map has no evidence of templates), `derive_new` (copies that joined a family since the previous refresh — the drift to act on now: derive the canon, don't queue an audit), `proposer` (what this refresh examined) — plus diff --git a/src/scribe/services/coverage.py b/src/scribe/services/coverage.py index dcb31ae..a16ff7a 100644 --- a/src/scribe/services/coverage.py +++ b/src/scribe/services/coverage.py @@ -294,6 +294,34 @@ _DYNAMIC_CLASS_RE = re.compile( ) # Svelte's directive form: class:active={cond}. _SVELTE_CLASS_RE = re.compile(r"(? makes Vue add .toast-enter-active et al at +# runtime, and React's does the same. A +# reader of `class=` attributes alone therefore calls every one of those +# rules unused, which is a false positive no amount of care in the +# stylesheet can avoid (#2970). A dynamic `:name="…"` stays unknowable. +_TRANSITION_NAME_RE = re.compile( + r"""<\s*[Tt]ransition(?:-[Gg]roup|Group)?\b[^>]*?(? list[str]: """The class tokens of a static attribute value: whitespace-split, only - well-formed names (an interpolation like `{{ cls }}` contributes none).""" - return [t for t in _MUSTACHE_RE.sub(" ", value).split() if _CLASS_TOKEN_RE.match(t)] + well-formed names. An interpolation (`{{ cls }}`, `${cls}`) is blanked + before the split, so a name built around one leaves its static head — + `status-` from `status-{{ s }}` — which is emitted as the prefix + reference `status-*` rather than as a class nothing is called.""" + out: list[str] = [] + for t in _MUSTACHE_RE.sub(" ", value).split(): + if not _CLASS_TOKEN_RE.match(t): + continue + if t.endswith(("-", "_")): + if len(t.rstrip("-_")) >= _PREFIX_MIN_STEM: + out.append(t + PREFIX_MARK) + continue + out.append(t) + return out def _dynamic_class_tokens(expr: str) -> list[str]: @@ -336,7 +376,13 @@ def class_references(path: str, text: str) -> dict[str, int]: files that carry no markup (by suffix). Reads the static `class=` / `className=` attributes, the Vue and React dynamic forms and Svelte's `class:x` directive; never a CSS selector (`.x {` is a definition, read - by extract_definitions) and never a script's `querySelector('.x')`.""" + by extract_definitions) and never a script's `querySelector('.x')`. + + Two forms name classes without spelling them out, and both are read + (#2970): a transition `name=` stands for every class the framework + generates from it, and a concatenated name contributes the prefix + reference `head-*` — which resolve_consumers matches against every row + whose symbol starts with `head-`.""" if not (path or "").lower().endswith(_TEMPLATE_SUFFIXES): return {} counts: dict[str, int] = {} @@ -351,6 +397,11 @@ def class_references(path: str, text: str) -> dict[str, int]: expr = next((g for g in m.groups() if g is not None), "") bump(_dynamic_class_tokens(expr)) bump([m.group(1) for m in _SVELTE_CLASS_RE.finditer(text)]) + for m in _TRANSITION_NAME_RE.finditer(text): + name = next((g for g in m.groups() if g is not None), "").strip() + if not _CLASS_TOKEN_RE.match(name): + continue + bump([name + suffix for suffix in _TRANSITION_SUFFIXES]) return counts diff --git a/src/scribe/services/shape_ledger.py b/src/scribe/services/shape_ledger.py index 3d155bc..02c9d33 100644 --- a/src/scribe/services/shape_ledger.py +++ b/src/scribe/services/shape_ledger.py @@ -279,20 +279,39 @@ def resolve_consumers( of that name when F defines it (a scoped rule is consumed by its own template); otherwise to every other file's row of that name — a shared sheet, or, when several files define it, all of them: the map says - "ambiguous" by fanning out rather than guessing one.""" + "ambiguous" by fanning out rather than guessing one. + + A token ending in ``PREFIX_MARK`` is a PREFIX reference (#2970) — the + static head of a name the template concatenates, `status-*` from + `` `status-${s}` ``. It stands for every row whose symbol starts with + that head, each resolved by the same own-file-else-fan-out rule. The + template cannot tell us WHICH of them it built, so the map credits all + of them rather than calling live rules unused.""" + # Lazy, like the extract_definitions import below: coverage reaches into + # this module during a refresh, so neither may import the other at load. + from scribe.services.coverage import PREFIX_MARK + by_symbol: dict[str, list[tuple[int, str]]] = {} for sid, path, symbol in css_rows: by_symbol.setdefault(symbol, []).append((sid, path)) out: dict[tuple[int, str], int] = {} + + def credit(rows: list[tuple[int, str]], consumer: str, count: int) -> None: + own = [sid for sid, path in rows if path == consumer] + for sid in own or [sid for sid, _path in rows]: + out[(sid, consumer)] = out.get((sid, consumer), 0) + int(count) + for consumer, tokens in references.items(): for token, count in tokens.items(): - rows = by_symbol.get(token) - if not rows: + if token.endswith(PREFIX_MARK): + head = token[: -len(PREFIX_MARK)] + for symbol, rows in by_symbol.items(): + if symbol.startswith(head): + credit(rows, consumer, count) continue - own = [sid for sid, path in rows if path == consumer] - targets = own or [sid for sid, _path in rows] - for sid in targets: - out[(sid, consumer)] = out.get((sid, consumer), 0) + int(count) + rows = by_symbol.get(token) + if rows: + credit(rows, consumer, count) return out diff --git a/tests/test_integration_shape_classify.py b/tests/test_integration_shape_classify.py index 12f06db..034cfdc 100644 --- a/tests/test_integration_shape_classify.py +++ b/tests/test_integration_shape_classify.py @@ -683,6 +683,58 @@ async def test_derive_new_names_the_copy_that_joined_a_family_since_the_stamp(se assert derive_new_summary(rows, since=None)["count"] == 0 +@pytest.mark.integration +async def test_transition_and_prefix_references_clear_the_unused_css_flag(seeded): + """#2970 end to end: the two class forms a template never spells out — + a transition `name=` and a concatenated name — travel from the real + extractor through resolution into the flag, so rules they reach stop + being reported as unused. Only a rule nothing can reach stays listed.""" + from scribe.services.coverage import class_references + from scribe.services.shape_ledger import sync_repo_consumers, used_by_map, live_rows + + pid, owner = seeded["pid"], seeded["owner"] + defs = _defs( + ("assets/anim.css", "css", "toast-enter-active", + ".toast-enter-active {", ".toast-enter-active { opacity: 0 }"), + ("assets/anim.css", "css", "toast-leave-to", + ".toast-leave-to {", ".toast-leave-to { opacity: 0 }"), + ("assets/state.css", "css", "status-done", + ".status-done {", ".status-done { color: green }"), + ("assets/state.css", "css", "status-todo", + ".status-todo {", ".status-todo { color: grey }"), + ("assets/state.css", "css", "really-dead", + ".really-dead {", ".really-dead { color: red }"), + ) + await sync_repo_shapes(pid, REPO, defs, seen_marker="t1") + + refs = { + # Vue applies the toast-* classes itself; the markup names none of them. + "v/Toast.vue": class_references( + "v/Toast.vue", '
  • ' + ), + # The board builds its status class; which one is unknowable. + "v/Board.vue": class_references("v/Board.vue", ''), + } + assert "toast-enter-active" in refs["v/Toast.vue"] + assert refs["v/Board.vue"] == {"status-*": 1} + + await sync_repo_consumers(pid, REPO, refs) + live = [r for r in await live_rows(pid) if r.kind == "css"] + used = await used_by_map(live) + by = {(r.path, r.symbol): used[r.id]["count"] for r in live} + assert by[("assets/anim.css", "toast-enter-active")] == 1 + assert by[("assets/anim.css", "toast-leave-to")] == 1 + # the prefix credits BOTH candidates — the template does not say which + assert by[("assets/state.css", "status-done")] == 1 + assert by[("assets/state.css", "status-todo")] == 1 + assert by[("assets/state.css", "really-dead")] == 0 + + unused, n = await list_project_shapes(owner, pid, flag="unused-css") + assert n == 1 and [(r.path, r.symbol) for r in unused] == [ + ("assets/state.css", "really-dead") + ] + + @pytest.mark.integration async def test_consumer_map_syncs_edges_from_template_references(seeded): """Milestone 302: the consumer edges follow the archive — own-file diff --git a/tests/test_pattern_coverage.py b/tests/test_pattern_coverage.py index f94a15c..d1cec30 100644 --- a/tests/test_pattern_coverage.py +++ b/tests/test_pattern_coverage.py @@ -152,6 +152,45 @@ def test_class_references_reads_vue_static_and_dynamic_forms_only(): } +def test_class_references_reads_framework_transition_names(): + """A transition `name=` is a class reference: the framework applies + `.toast-enter-active` and friends at runtime, so a stylesheet that + defines them is consumed even though no template spells one out (#2970). + Every spelling of the tag counts; a bound `:name` stays unknowable.""" + refs = class_references( + "a/T.vue", + '
    ', + ) + for suffix in ("-enter-from", "-enter-active", "-leave-to", "-move"): + assert refs["toast" + suffix] == 1, suffix + assert refs["toast-item"] == 1 # the static attribute still counts + + assert "peek-enter-active" in class_references("a/T.vue", '') + assert "g-move" in class_references("a/T.vue", '') + # React's CSSTransition names the same idea with a different suffix set + react = class_references("a/T.jsx", '') + assert {"fade-enter-active", "fade-exit-active", "fade-exit-done"} <= set(react) + # A bound name is a variable, not a name we can read + assert class_references("a/T.vue", '') == {} + + +def test_class_references_reads_a_concatenated_name_as_a_prefix(): + """`status-${s}` cannot be resolved to one class, but its static head is + real information: it is emitted as the prefix reference `status-*` so + the rows it could have built are not reported unused (#2970). A head too + short to mean anything, or a bare separator, says nothing.""" + vue = ( + '
    ' + "" + '' + ) + assert class_references("a/P.vue", vue) == {"status-*": 1, "pri-*": 1} + # a server template interpolating into the middle of a name, same reading + assert class_references("t/p.html", '') == {"card-*": 1} + # an ordinary name never picks up the marker + assert class_references("a/P.vue", '