Sheet Toolbar Audit — Variance Matrix & Inconsistency Review
Purpose: Document every toolbar button + dropdown action on the workbook sheet view, the predicates that decide whether it appears, and the inconsistencies that surface when you cross-multiply by sheet variance. This is the review-first doc requested before any reorganization — proposed fixes live in §6, no code changes yet.
Source files:
- View: apps/hr-system/src/views/tabs/sheets.html
- Predicates: apps/hr-system/src/sheets/workbook/ — the workbook component was split into per-concern slices in 2026-06 (
columns.ts,payroll.ts,cell-edit.ts,sidebar.ts, …), composed by workbook.sheet.ts. Method/getter names referenced below are unchanged but now live in slice files; bare line numbers in this doc predate the split. See workbook-component-architecture.md.
1. Sheet Variance Dimensions
A sheet's behavior is the cross-product of these axes. Buttons need to consider any subset.
1.1 Sheet-level metadata (persisted)
| Field | Values | Purpose |
|---|---|---|
kind | allowance / deduction / mixed / overtime / incentive / therapy_daily | Domain — what the rows mean |
viewKind | row (default) / transposed / matrix | Render layout |
visibility | private / shared | Who else can see it |
isDefault | bool | Server-seeded "system" sheet vs user-created |
rowBasis | employee / date / free (nullable; auto-detected if null) | What + 행 추가 prompts for |
autoPopulate | { source: "employee" | ... } or null | Source-bound row generation |
lockedAt / lockedBy | timestamp / uuid | Manual sheet lock (distinct from period freeze) |
groupId | string | null | Sidebar group |
description | string | null | Inline subtitle |
year, month | int | Active period |
1.2 Derived sheet states (computed getters)
| Getter | True when | Used by |
|---|---|---|
isReadOnly | Currently hardcoded false (line 3086) — predicate exists but no-ops | Many x-show predicates — broken gating, see §6.1 |
isPayrollLedger | name contains 급여대장 OR autoPopulate.source === "employee" OR any column is sourceTable=employee / kind=aggregate | 💰 / 🔓 / 📥 buttons |
activeSheetIsDateBased | rowBasis === "date" OR (no identity cols + has date-format col) | Row-add prompt shape |
activeSheetIsFreeBasis | rowBasis === "free" (explicit only) | Skips bulk-add |
activeSheetIsTreatmentSession | (custom predicate — therapy_daily + extras) | 📅 / 🔄 sync sessions |
allLedgerRowsConfirmed | every employeeRow has a CONFIRMED/PAID PayrollRecord | toggles 💰 ↔ 🔓 + reveals 📥 |
sheetLocked | lockedAt set | 🔒 banner + menu label flip |
hasComputedColumn | any col has kind=computed + computeSourceModule + computedKey | 🧮 compute button |
hasCommitColumn | any col has commitToKind + matching item code (allowanceItemCodeId/deductionItemCodeId) | ✓ commit button |
hasRefreshableSource | autoPopulate set OR any col has kind=aggregate or sourceTable !== "employee" | 🔄 refresh button |
1.3 Row-level state (drives count badges)
| State | Meaning |
|---|---|
draftRows.length | Unsaved insert-row drafts |
pendingRowCount | Rows with status=pending |
approvedRowCount | Rows with status=approved |
selectedRowCount | Cells selected (anything > 1 unlocks bulk-row actions) |
unresolvedMemoCount | Memos with status open |
1.4 Column kinds (per-column variance)
From columnEffectiveKind:
| Kind | Meaning | Editor behavior |
|---|---|---|
identity | 사번 / 성명 / 팀 — relink an existing record to another employee | Employee autocomplete picker |
literal | Plain value typed directly into the cell | Text input |
ref | Reference into a source-record field (e.g. amount) | Text input + per-kind validator |
formula | User-typed formula expression (e.g. =ROUNDUP((TODAY()-…)/365)) | Formula bar (planned) |
computed | codeModule-driven calculation | Read-only; recomputed by 🧮 |
aggregate | SUM across other sheets / records | Read-only; refreshed by 🔄 |
Column-level toolbar gates: hasComputedColumn, hasCommitColumn, hasRefreshableSource (see 1.2).
2. Toolbar Inventory — Top Bar (above sheet body)
2.1 Period strip (always visible, top-left)
| Button | Predicate | Notes |
|---|---|---|
Year <select> | always | |
Month <select> | always | |
이번달 badge | isCurrentPeriod | Static label |
이번달 button | !isCurrentPeriod | Jump |
+ 새 시트 | always | Opens new-sheet modal |
📋 이전 월에서 가져오기 | always | Carry forward |
? shortcuts help | always | Toggle cheat-sheet |
2.2 Sidebar header (when sidebar open)
| Button | Predicate | Notes |
|---|---|---|
| 그룹 관리 | always | Opens group manager modal |
| 템플릿 관리 | always | Opens template manager modal |
| 정렬 모드 | always | Drag-to-reorder toggle |
| Per-sheet pin (📌) | per-row | Pin in sidebar |
| 🔒 lock icon | s.lockedAt | Sidebar row badge |
| 비공개 (🔒) | s.visibility === 'private' | Sidebar row badge |
| 행렬 / 전치 (viewKind badge) | s.viewKind && s.viewKind !== 'row' | Sidebar row badge |
YM badge ('25.5) | s.year && s.month | Sidebar row badge |
3. Toolbar Inventory — Sheet Header (with active sheet)
The sheet header is the main pain point. Buttons cluster into four logical "zones," but the current placement doesn't always honor that.
3.1 Identity & navigation (left side)
| Button | Predicate | Zone | Notes |
|---|---|---|---|
Sidebar collapse ◀ / ▶ | always | Nav | |
Sheet title (activeSheet.name) | activeSheet | Identity | dblclick = rename when !isReadOnly |
| YM pill | activeSheet | Identity | dblclick = edit period when !isReadOnly |
Close sheet ✕ | activeSheet | Nav | |
updatedAt timestamp | activeSheet?.updatedAt | Identity | |
+ 설명 추가 | always (label changes) | Identity | |
Row count n/N건 | always | Identity |
3.2 Filter & search (middle)
| Button | Predicate | Zone | Notes |
|---|---|---|---|
상태 <select> | always | Filter | pending/approved/paid/cancelled |
검색 <input> (/) | always | Filter | |
검색 <clear> × | hasSearch | Filter | |
| Cross-sheet popover | hasSearch && !hidden | Filter | |
🔒 잠긴 시트 badge | sheetLocked | Identity (badge) | Read-only signal |
3.3 DATA actions (right of search) — the contested zone
| Button | Predicate | Sheet kinds affected | Notes |
|---|---|---|---|
| 🧮 계산 실행 | !isReadOnly && hasComputedColumn | any | Runs codeModule on computed cols |
| ✓ 커밋 | !isReadOnly && hasCommitColumn | any | Writes sheet values → records |
| 🔄 새로고침 | hasRefreshableSource | any | Re-syncs from server-bound cols |
| 💰 전체 확정 | !isReadOnly && isPayrollLedger && !allLedgerRowsConfirmed | ledger only | Bulk PayrollRecord confirm |
| 🔓 전체 취소 | !isReadOnly && isPayrollLedger && allLedgerRowsConfirmed | ledger only | Toggles with 💰 |
| 📥 이카운트 Export | isPayrollLedger && allLedgerRowsConfirmed | ledger only | XLSX download |
Note: No visual separator between "data" and "payroll" actions — they sit shoulder-to-shoulder. The mental model is unclear: 🧮 is for this sheet's columns, but 💰 is for this sheet's rows treated as ledger. Same icon family, different scope.
3.4 Zone separator → ROW actions
| Button | Predicate | Notes |
|---|---|---|
| divider | activeSheet && !isReadOnly | |
| ➕ 행 추가 | !isReadOnly && activeSheet | Tooltip changes by activeSheetIsFreeBasis / activeSheetIsDateBased |
| 📝 새 항목 | !isReadOnly && activeSheet | "auto-fill" entry |
| 📅 새 세션 | !isReadOnly && activeSheet | Always shown — even on non-therapy sheets (see §6.3) |
| 👥 일괄 추가 | !isReadOnly && activeSheet && !activeSheetIsDateBased && !activeSheetIsFreeBasis | |
| 🔄 인센 동기화 | !isReadOnly && activeSheet && activeSheetIsTreatmentSession | Treatment-specific |
3.5 DRAFT actions (conditional cluster)
Wrapped in <template x-if="draftRows.length > 0">:
| Button | Notes |
|---|---|
| divider | |
| 💾 모두 저장 (badge: count) | |
| ✖ 모두 취소 |
3.6 WORKFLOW actions (conditional cluster)
Wrapped in <template x-if="!isReadOnly && (activeSheet || pendingRowCount > 0 || approvedRowCount > 0 || selectedRowCount > 1)">:
| Button | Predicate | Notes |
|---|---|---|
| divider | (outer wrapper) | |
| 🧮 재계산 | !isReadOnly && activeSheet | Icon collision with 🧮 계산 실행 (3.3) — different action |
| ✅ 승인 (badge: pendingRowCount) | !isReadOnly && pendingRowCount > 0 | |
| 💸 지급 (badge: approvedRowCount) | !isReadOnly && approvedRowCount > 0 | |
| 🗑 삭제 (badge: selectedRowCount) | !isReadOnly && selectedRowCount > 1 |
3.7 Notifications / panel toggles (right side)
| Button | Predicate | Notes |
|---|---|---|
| 💬 메모 이력 (badge: unresolvedMemoCount) | activeSheet | |
◀ / ▶ Detail panel toggle | always |
3.8 Overflow menu ⋯
| Item | Predicate | Notes |
|---|---|---|
| 🛠 시트 설정 | activeSheet | Always shown |
| --- | ||
| 📋 TSV 가져오기 | !isReadOnly && activeSheet | |
| 📥 내보내기 (CSV / XLSX) | always | |
| --- | ||
| 🔓/🔒 잠금 | !periodLocked | Label flips on sheetLocked |
| 🌐/🔐 공유 | !isReadOnly && activeSheet | Label flips on visibility |
| 📋 복제 | !isReadOnly | |
| 💾 템플릿으로 저장 | !isReadOnly && activeSheet | |
| --- | ||
| 🗑 삭제 | !isReadOnly | No isDefault guard — system templates are deletable (see §6.2) |
3.9 Column settings (rightmost)
| Button | Notes |
|---|---|
열 설정 | Toggle a dropdown listing all columns with visibility + kind chip |
| Per-column visibility checkbox | |
Per-column 식별/값/참조/계산/수식 chip | Opens column-kind editor modal |
4. Icon Vocabulary — Conflicts & Categories
4.1 Icons in use
| Icon | Used for | Locations |
|---|---|---|
| ➕ | Add row | 3.4 |
| 📝 | New auto-fill entry | 3.4 |
| 📅 | New treatment session | 3.4 |
| 👥 | Bulk add employees | 3.4 |
| 🧮 | (a) Run compute (b) Recalculate incentives | 3.3 + 3.6 ← collision |
| ✓ | Commit | 3.3 |
| 🔄 | (a) Refresh auto-populate (b) Sync treatment sessions | 3.3 + 3.4 ← collision |
| 💰 | Confirm ledger | 3.3 |
| 🔓 | (a) Cancel confirmations (b) Unlock sheet (menu) | 3.3 + 3.8 ← collision |
| 🔒 | (a) Locked banner (b) Lock sheet (menu) (c) Private (sidebar) | 3.2, 3.8, 2.2 |
| 📥 | (a) eCount export (b) CSV/XLSX export (menu) | 3.3 + 3.8 — same intent, OK |
| 💾 | (a) Save drafts (b) Save as template (menu) | 3.5 + 3.8 |
| 💬 | Memo history | 3.7 |
| ✅ | Approve pending | 3.6 |
| 💸 | Mark paid | 3.6 |
| 🗑 | (a) Delete rows (b) Delete sheet (menu) | 3.6 + 3.8 |
| 📋 | (a) Carry forward (b) TSV import (menu) (c) Duplicate (menu) | 2.1 + 3.8 + 3.8 — three meanings |
| 🛠 | Sheet settings | 3.8 |
| 🌐 / 🔐 | Visibility | 3.8 |
| ✖ | Cancel drafts | 3.5 |
| ✕ | Close active sheet | 3.1 |
| ◀ / ▶ | Sidebar / detail panel toggle | 3.1, 3.7 |
| ⋯ | Overflow menu | 3.8 |
/ ? | Search focus / help | 2.1 keyboard |
4.2 Logical categories
If we re-bucket by user intent, the toolbar wants to read:
- Navigation — sidebar toggle, detail panel toggle, close sheet, sheet title/edit
- Filter — status filter, search, cross-sheet popover
- Compute/refresh — 🧮 compute, 🔄 refresh — applies to columns
- Commit — ✓ commit, 💰 confirm ledger, 🔓 cancel — applies to rows (ledger)
- Export — 📥 eCount, 📥 CSV (today in menu)
- Row creation — ➕, 📝, 📅, 👥
- Draft management — 💾 / ✖
- Row workflow — 🧮 recalc, ✅, 💸, 🗑
- Notifications — 💬 memo
- Setup / rare — overflow menu items
- Per-column setup — 열 설정 dropdown
Categories 3 + 4 are the most contested (data vs commit vs ledger semantics overlap), and 6 is the most kind-sensitive (which row-add affordance applies depends on sheet kind + rowBasis).
5. Cross-Type Variance — Which Buttons Should Appear Where
This is the matrix we can argue from. ✅ = appropriate · ⚠️ = currently shown but probably shouldn't · ❌ = hidden/inapplicable · ❓ = unclear.
5.1 By sheet kind
| Button | allowance | deduction | mixed | overtime | incentive | therapy_daily |
|---|---|---|---|---|---|---|
| 🧮 계산 실행 | ✅ if computed col | same | same | same | same | same |
| ✓ 커밋 | ✅ if commit col | same | same | same | same | same |
| 🔄 새로고침 | ✅ | same | same | same | same | same |
| 💰 / 🔓 / 📥 ledger | ❌ unless ledger | ❌ | ❌ | ❌ | ❌ | ❌ |
| ➕ 행 추가 | ✅ | ✅ | ✅ | ✅ | ✅ | ❓ |
| 📝 새 항목 | ✅ | ✅ | ✅ | ✅ | ✅ | ❓ |
| 📅 새 세션 | ⚠️ shows | ⚠️ shows | ⚠️ shows | ⚠️ shows | ⚠️ shows | ✅ |
| 👥 일괄 추가 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ (date-based) |
| 🔄 인센 동기화 | ❌ | ❌ | ❌ | ❌ | ❓ | ✅ |
| 🧮 재계산 (3.6) | ❓ generic? | ❓ | ❓ | ❓ | ✅ "총금액=건수×단가" only matches incentive | ❓ |
| ✅ 승인 / 💸 지급 | ✅ if status field exists | same | same | same | same | ❓ |
| 🗑 삭제 행 | ✅ | ✅ | ✅ | ✅ | ✅ | ❓ |
Tooltip note: 📅 새 세션's tooltip says "새 치료 세션 등록" — implies therapy_daily, but the button shows on every sheet kind because the predicate is just !isReadOnly && activeSheet.
5.2 By isDefault (system template vs user-created)
| Action | System sheet | User sheet | Issue |
|---|---|---|---|
| 🗑 시트 삭제 | ⚠️ allowed | ✅ allowed | Should block / warn for isDefault — these are seeded; deletion likely silently re-seeds on next reload |
| 💾 템플릿으로 저장 | ⚠️ shows | ✅ shows | Saving a system sheet as a user template duplicates seed data |
| 시트 이름 변경 (dblclick) | ⚠️ shows | ✅ shows | Renaming a system sheet may break the auto-upsert by name (e.g., 급여대장 detection) |
| 시트 설정 (🛠) | ✅ but restricted? | ✅ | Some fields (kind, columnLayout) might be locked on system sheets |
| 📋 복제 | ✅ | ✅ | Fine |
5.3 By lockedAt (manual sheet lock)
| Action | Sheet locked | Notes |
|---|---|---|
| All write actions | gated by isReadOnly | but isReadOnly is hardcoded false — so lock is purely visual (line 3086) |
| 🔓 잠금 해제 (menu) | always available |
5.4 By column kind
| Has column kind… | Button surfaces |
|---|---|
computed | 🧮 계산 실행 |
with commitToKind + matching item code | ✓ 커밋 |
aggregate or sourceTable !== "employee" or autoPopulate | 🔄 새로고침 |
formula | no UI yet — the kind chip says "수식" but there's no formula bar / preview / error surfacing (see §6.6) |
5.5 By viewKind
The button bar is identical across row / transposed / matrix views. Most row-oriented actions (➕, 🗑, 승인, etc.) still make sense, but the affordances might benefit from per-view labels (e.g. "+ 열 추가" on transposed).
6. Inconsistencies & Proposed Fixes
Each finding paired with the smallest change that resolves it.
6.1 isReadOnly is a no-op
workbook/ — the isReadOnly getter was hardcoded return false, so every x-show="!isReadOnly && …" in the view is dead-weight gating; manual sheetLocked doesn't gate writes and period-level lock doesn't either. (Note: isReadOnly is no longer present in the code as of the 2026-06 slice split — this finding appears already resolved; confirm before acting on §6.1 below.)
Proposed: Either (a) remove isReadOnly from the toolbar entirely and rely on explicit gates (sheetLocked, isDefault, period.confirmed), or (b) restore the gating logic. Need product input on the intended lock semantics — current code looks like a half-revert.
6.2 System templates can be deleted / renamed / re-saved-as-template
System (isDefault: true) sheets carry no UI guard. The auto-upsert on next load might mask deletion, but renaming would silently break the name?.includes("급여대장") heuristic that drives isPayrollLedger.
Proposed:
- Hide
🗑 삭제from the overflow menu whenisDefault: true. (Show a복제 후 편집하세요hint instead.) - Hide
💾 템플릿으로 저장forisDefault: true. - Disable
dblclick = renameon the title forisDefault: true. - Surface a small
시스템 시트chip next to the title.
6.3 📅 새 세션 shows everywhere
The button reads "새 세션 — 새 치료 세션 등록" but the predicate is just !isReadOnly && activeSheet. On allowance/deduction/overtime sheets it's noise.
Proposed: Gate by activeSheetIsTreatmentSession. Same predicate already used for 🔄 인센 동기화 right below.
6.4 🧮 means two different things
- Toolbar zone 3.3: "계산 실행 — codeModule recomputes computed columns"
- Toolbar zone 3.6: "재계산 — incentive 총금액 = 건수 × 단가"
They sit ~3 buttons apart and use the same icon. Users can't tell them apart at a glance.
Proposed:
- Keep 🧮 for the column-level compute (3.3).
- Change 3.6 to ✖️→🔄 or rename to a more specific verb, e.g., "✦ 인센 정합" or merge it into the per-row right-click menu since it's an incentive-only operation (tooltip already says "인센티브 행에 대해").
6.5 🔄 means three different things across the toolbar
- 3.3 — refresh auto-populate (server resync of bound columns)
- 3.4 — sync treatment sessions (pull from 결제툴)
- (Bonus: keyboard
/and?share modifier semantics)
Same icon, different scopes.
Proposed:
- 3.3 stays 🔄 (generic server refresh).
- 3.4 becomes 🔁 or ⇪ or an explicit text label "세션 ↻" since it's a more specific external sync.
6.6 formula kind has no editor
columnEffectiveKind recognizes formula, the column-kind chip shows "수식", but there's no formula bar, no expression input, no evaluation. The =ROUNDUP(…) use case the user mentioned needs:
- A formula input field (in the column-kind editor)
- A live preview/evaluation
- Per-row error states
Proposed: This is a separate feature (the user's earlier interrupted message). Out of scope for the toolbar audit but adjacent to the column-kind chip in 3.9. Tracking as a follow-up: §7.
6.7 Overflow menu order is inconsistent
3.8 reads top-to-bottom:
- 🛠 시트 설정
- 📋 TSV 가져오기, 📥 내보내기
- 🔒 잠금, 🌐 공유, 📋 복제, 💾 템플릿으로 저장
- 🗑 삭제
The middle cluster mixes share-related (잠금/공유) with duplication (복제/템플릿). And 📋 is used for both 이전 월에서 가져오기 (top toolbar) and TSV 가져오기 and 복제 — three different meanings.
Proposed: Re-cluster as:
- Setup — 🛠 시트 설정
- Data I/O — 📋 TSV 가져오기, 📥 내보내기
- Sharing — 🔒 잠금, 🌐 공유
- Lifecycle — ⎘ 복제, 💾 템플릿으로 저장
- Destructive — 🗑 삭제
And re-icon 📋 복제 → ⎘ or a different glyph to break the icon collision.
6.8 No visible cue for viewKind !== "row"
The sidebar shows a small chip but the active toolbar doesn't. Users may not realize a sheet is in transposed or matrix mode until they look at the body.
Proposed: Add a small 행렬 / 전치 pill next to the YM pill on the title row when viewKind !== "row".
6.9 Draft cluster and Workflow cluster repeat dividers
3.5 and 3.6 each render their own | divider. When both are visible (draft rows + pending rows on the same sheet), you see … | 💾 ✖ | 🧮 ✅ 💸 🗑 … — two adjacent dividers visually flatten. Minor.
Proposed: Move the divider to a single shared rule that only appears when any row-cluster is visible.
6.10 🔒 잠긴 시트 badge sits in the search row but the action lives in the menu
Discoverability problem: the badge tells you the sheet is locked, but to unlock you have to find the overflow menu. The badge isn't a button.
Proposed: Make the badge clickable — clicking it opens the menu pre-scrolled to the lock toggle, OR directly toggles when the user has permission.
7. Out-of-scope (track for follow-up)
- Formula column UX — §6.6. Needs an expression editor, evaluator binding, and per-cell error display.
isReadOnlysemantics — §6.1. Product decision needed about what locks writes.- Cross-sheet popover keyboarding — currently mouse-only.
- Per-
viewKindrow-add affordances —+ 행 추가vs+ 열 추가on transposed sheets.
8. Recommended Review Order
When discussing fixes, take them in this order — earlier items unblock later ones:
- §6.1 (
isReadOnly) — decide gating semantics first; the answer constrains many other predicates. - §6.2 (system templates) — single decision (block destructive ops on
isDefault) covers four buttons. - §6.3–6.5 (icon collisions / wrong scope) — pure visibility fixes, no backend touched.
- §6.7 (menu order) — UX-only, low risk.
- §6.8 / §6.9 / §6.10 — minor polish.
- §6.6 + §7 (formula UX) — its own feature track.
9. Decisions Log
Track product decisions as they're made, with date + rationale. Future-you doesn't relitigate.
| Date | Item | Decision | Rationale |
|---|---|---|---|
| 2026-05-20 | §6.1 isReadOnly semantics | Remove entirely. Delete the getter; rely on explicit gates (sheetLocked, isDefault on templates only, per-row PayrollRecord.status). | Currently a no-op (return false). Removing dead gating is purely a cleanup — no behavior change. Each surviving gate has one job. |
| 2026-05-20 | §6.2 System-template restrictions | N/A — system sheets are not a thing. A sheet derived from a system template is a user sheet from that point on. "System" applies only to templates. No isDefault flag on sheets; no 시스템 시트 chip; no per-sheet guards. | Clarifies the data model: templates are seeded; sheets are always user-owned. Avoids inventing UI for a concept that shouldn't exist. |
| 2026-05-20 | §6.10 🔒 badge click behavior | Open overflow menu pre-scrolled to the lock toggle. | Prevents accidental unlock; consistent with how other destructive actions live in the overflow menu. |
10. PR Plan
PR 1 — Icon polish ✅ shipped 2026-05-20
No backend, no predicate logic changes. Pure view-layer fixes.
- §6.3 — Gate
📅 새 세션onactiveSheetIsTreatmentSession - §6.4 — Row-workflow
🧮 재계산→⟳ 인센 정합(resolves icon collision with🧮 계산 실행) - §6.5 — Treatment session sync
🔄→↻+ label "세션 가져오기" - §6.7 — Overflow menu re-clustered into Setup / I/O / Sharing / Lifecycle / Destructive;
📋 복제→⎘ 복제 - §6.8 —
전치/행렬chip next to YM pill whenviewKind !== "row" - §6.9 — Single shared divider before draft + workflow clusters
PR 2 — Gating cleanup + lock badge
Scope locked in by §9 decisions (2026-05-20).
- §6.1 — Remove
isReadOnlygetter + every!isReadOnly &&predicate in the view + everyif (this.isReadOnly) returnin workbook.sheet.ts. Pure cleanup; no behavior change because the getter was alreadyfalse. - §6.2 — Audit any lingering references to
isDefaulton sheets (vs templates). If found, treat as bugs. No new UI work for this item. - §6.10 —
🔒 잠긴 시트badge becomes a button that opens the overflow menu. Optional polish: briefly highlight the lock toggle row in the menu after opening, so the user's eye lands there.
Estimated scope: ~50 lines of removals + ~10 lines for the lock-badge wiring.
PR 3 — Formula column UX 🔭 separate feature track
§6.6 — Formula bar, evaluator binding, per-cell error display, autocomplete for column refs.
Estimated scope: larger; involves a new modal / inline bar, a sandboxed expression evaluator, and cell-level error states. Should be its own design pass before any code.