feat(side-panel): pin the import table columns up to Billable - #127
Merged
Conversation
The ManicTime import table grows one column per tracked day, so booking an entry near the end of the month scrolled the identifying columns off screen and the row a▶️ button belonged to was no longer visible. Everything left of the first date column now stays pinned while the date columns scroll underneath: #, Template, every Tag column, Notes and Billable. Widths are fixed constants, so the left offsets are a plain running sum instead of a DOM measurement. The even-row stripe becomes opaque (same rendered colour) — a translucent background would let the scrolling cells bleed through the pinned ones. Closes #12
…only the table The fixed column widths clipped tag names even when the panel had room to spare, and the panel itself carried the horizontal scrollbar. Columns now keep their natural widths and nothing clips, so a value is never cut off — long values wrap instead. That makes the offsets unknowable ahead of time, so useFrozenColumnOffsets measures the header and publishes them as --frozen-left-<position> custom properties on the table. The table gets its own scroll container, which means the panel no longer scrolls sideways and the pinned columns stick to the table's edge. Its max-height is what keeps the header row sticky, since a sticky element sticks inside its scroll container. body is no longer a flex container (a Vite template leftover): as a flex item #root was sized by its content, so the wide table pushed the whole panel wider regardless of the wrapper's overflow.
The "How to use this" list is long enough to push the import table itself off screen. It is a Collapse panel now, closed by default, so it only costs its header row until it is asked for.
"Delete saved data" sat on its own line below the table. It now shares the row with the auto-map button and the notes switch, pushed to the far right — one row of controls above the table instead of two blocks around it, and the destructive action stays visually apart from the two next to it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #12.
Problem
The ManicTime import table grows one column per tracked day. Booking an entry near the end of the month scrolls the identifying columns off screen, so you click a▶️ button without seeing which row it belongs to. The header row already stays put vertically; the horizontal axis had nothing.
What changed
Everything left of the first date column now stays pinned while the date columns scroll underneath:
#,Template, everyTag n,NotesandBillable. A header without any date column pins nothing.frozenColumns.ts— new pure function, decides which columns are pinned and at whichleftoffset. Widths are fixed constants, so the offsets are a running sum instead of a DOM measurement, which keeps them testable in jsdom.DATE_COLUMN_REGEXmoved here fromImportEntriesTableCell, which now imports it — both places have to agree on where the frozen block ends.ImportEntries.css— theposition: stickyrules, a shadow marking the right edge of the block, andbox-sizing: border-boxon pinned cells (without it a cell renders 11 px wider than its width constant and each column overlaps its left neighbour).#3276b44a; a translucent background lets the scrolling cells bleed through the pinned ones. Replaced by the already-blended#a9c6df/#2e4358, so the rendered colour is unchanged — including in the Templates table, which the same unscoped rule styles.Known limitation
The pinned block is as wide as the columns it contains — a four-tag import is 546 px, wider than a narrow side panel. Documented in the "How to use this" alert and in
CLAUDE.md: drag the panel wider.Testing
test/frozenColumns.test.ts(new, 6 cases) pins the arithmetic and the edge cases: no date column, empty header, header starting with a date, slash-separated dates.test/importEntries.test.tsx(2 new cases) checks the wiring — header, data rows and footer row all carry the offsets, date columns carry none.npm run typecheck,npm run lint,npm run test:fast(256 tests) all pass.docs/architecture/testing.md.Design:
docs/superpowers/specs/2026-08-05-frozen-import-columns-design.md.