Conversation
- Adjusted color values in colors.py to align with Apple Human Interface Guidelines. - Enhanced text colors for better readability in dark mode. - Added new status colors for project tracking. - Modified dimensions in dimens.py for a more spacious layout. - Increased font sizes in fonts.py for improved readability and hierarchy. - Updated theme visual density to standard for a more balanced UI. - Changed TimeTrackingView heading size for better prominence. - Expanded test coverage in test_app_start.py to include new client and contact views. - Added resource-module attribute checks to ensure consistency in UI constants. - Implemented runtime tests for new Client and Contact side panels to validate content building methods.
There was a problem hiding this comment.
Pull request overview
This PR updates Tuttle’s UI layer toward a more “macOS native” look/feel and introduces new UI smoke tests intended to catch runtime regressions in view construction and resource-token usage.
Changes:
- Refactors entity list UIs from card/grid layouts into single-line row lists with inline detail/edit expansions (Projects/Contracts/Clients/Contacts).
- Updates design tokens (fonts, colors, dimensions) and adjusts headings/visual density to match the revised UI hierarchy.
- Adds a new status bar manager module and expands startup/UI smoke tests.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
tuttle_tests/test_app_start.py |
Adds import/export/resource-token smoke tests plus runtime construction exercises for side panels and controls. |
tuttle/app/timetracking/view.py |
Updates heading size token for the new typographic scale. |
tuttle/app/res/theme.py |
Adjusts app theme density to STANDARD. |
tuttle/app/res/fonts.py |
Introduces revised font sizing/weights and adds STATUS_BAR_SIZE. |
tuttle/app/res/dimens.py |
Tweaks layout constants (toolbar/footer/sidebar sizes, radii, card spacing) and adds status bar spacing tokens. |
tuttle/app/res/colors.py |
Updates semantic dark theme tokens and adds status/badge colors used by new UI. |
tuttle/app/projects/view.py |
Replaces project cards with table-style rows and adds ProjectSidePanel for inline detail/edit. |
tuttle/app/invoicing/view.py |
Adjusts filter/heading styling to align with new visual hierarchy. |
tuttle/app/home/view.py |
Simplifies toolbar and adds status-bar update logic driven by current view context. |
tuttle/app/core/views.py |
Introduces EntitySidePanel, refactors filters into chip tabs, and converts CrudListView to ListView with inline expansion. |
tuttle/app/core/status_bar.py |
Adds new interactive status bar component/manager. |
tuttle/app/contracts/view.py |
Replaces contract cards with rows and adds ContractSidePanel for inline detail/edit. |
tuttle/app/contacts/view.py |
Replaces contact cards with rows and adds ContactSidePanel for inline detail/edit. |
tuttle/app/clients/view.py |
Replaces client cards with rows and adds ClientSidePanel for inline detail/edit. |
app.py |
Removes now-unused route wiring for project/contract editor/detail screens and tweaks snack + route caching behavior. |
Comments suppressed due to low confidence (1)
tuttle/app/home/view.py:300
- HomeScreen creates and updates a StatusBarManager, but build() still renders a separate static
self.status_barContainer. As a result the manager'sbuild()/try_update()never affects anything visible. Replace the static status bar withself.status_bar_manager.build()(and keep a reference to that control), so_update_status_bar_for_view()updates the rendered status bar.
# Status bar manager
self.status_bar_manager = StatusBarManager(
on_click_overdue=lambda e: self._jump_to_invoicing(),
on_click_outstanding=lambda e: self._jump_to_invoicing(),
)
# Toolbar (slim, no title — view heading is the title)
self.toolbar = get_toolbar(
on_click_new_btn=self.on_click_add_new,
on_click_profile_btn=self.on_click_profile,
on_view_settings_clicked=self.on_view_settings_clicked,
)
def _on_sidebar_item_selected(self, item: views.NavigationMenuItem):
"""Called when the user clicks a sidebar nav item."""
self._selected_flat_index = self._all_items.index(item)
self.destination_view = item.destination
self.destination_content_container.content = self.destination_view
self._update_status_bar_for_view(item.label)
self.update_self()
# ── Action buttons ────────────────────────────────────────
def on_click_add_new(self, e):
item = self._all_items[self._selected_flat_index]
if item.on_new_intent:
self.pass_intent_to_destination(item.on_new_intent)
elif item.on_new_screen_route:
self.navigate_to_route(item.on_new_screen_route)
def on_resume_after_back_pressed(self):
if self.destination_view and isinstance(self.destination_view, TView):
self.destination_view.on_resume_after_back_pressed()
def pass_intent_to_destination(self, intent: str, data=None):
if self.destination_view and isinstance(self.destination_view, TView):
self.destination_view.parent_intent_listener(intent, data)
def on_view_notifications_clicked(self, e):
self.show_snack("not implemented", True)
def on_view_settings_clicked(self, e):
self.navigate_to_route(res_utils.PREFERENCES_SCREEN_ROUTE)
def on_click_profile(self, e):
self.navigate_to_route(res_utils.PROFILE_SCREEN_ROUTE)
# ── Build ─────────────────────────────────────────────────
def build(self):
self.destination_content_container = Container(
padding=Padding.all(dimens.SPACE_MD),
content=self.destination_view,
expand=True,
)
# Status bar — VS Code style thin bar at bottom
self.status_bar = Container(
height=dimens.FOOTER_HEIGHT,
bgcolor=colors.bg_statusbar,
padding=Padding.symmetric(horizontal=dimens.SPACE_SM),
content=Row(
controls=[
Text("Tuttle", size=11, color=colors.text_inverse),
],
alignment=MainAxisAlignment.START,
vertical_alignment=CrossAxisAlignment.CENTER,
),
)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Text( | ||
| project.title or "", | ||
| size=fonts.BODY_1_SIZE, | ||
| color=colors.text_primary, | ||
| weight=fonts.BOLD_FONT if is_selected else None, | ||
| overflow="ellipsis", | ||
| max_lines=1, | ||
| expand=True, |
There was a problem hiding this comment.
These Text controls set overflow="ellipsis" as a string. Elsewhere in the codebase (e.g. invoicing view) overflow is set using TextOverflow.ELLIPSIS. To avoid API/type mismatches across Flet versions, consider switching to the enum (overflow=TextOverflow.ELLIPSIS) and importing TextOverflow from flet.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| Text( | ||
| contract.title or "", | ||
| size=fonts.BODY_1_SIZE, | ||
| color=colors.text_primary, | ||
| weight=fonts.BOLD_FONT if is_selected else None, | ||
| overflow="ellipsis", | ||
| max_lines=1, | ||
| expand=True, | ||
| ), |
There was a problem hiding this comment.
These Text controls set overflow="ellipsis" as a string. Elsewhere in the codebase (e.g. invoicing view) overflow is set using TextOverflow.ELLIPSIS. To avoid API/type mismatches across Flet versions, consider switching to the enum (overflow=TextOverflow.ELLIPSIS) and importing TextOverflow from flet.
| def update_warnings( | ||
| self, | ||
| overdue_count: int = 0, | ||
| outstanding_text: str = "", | ||
| expiring_count: int = 0, | ||
| ): | ||
| """Update the business health warning indicators.""" | ||
| has_warnings = overdue_count > 0 or bool(outstanding_text) or expiring_count > 0 | ||
|
|
||
| # Overdue invoices | ||
| if overdue_count > 0: | ||
| self.overdue_item.set_text(f"{overdue_count} overdue") | ||
| self.overdue_item.visible = True | ||
| else: | ||
| self.overdue_item.visible = False | ||
|
|
||
| # Outstanding amount | ||
| if outstanding_text: | ||
| self.outstanding_item.set_text(outstanding_text) | ||
| self.outstanding_item.visible = True | ||
| else: | ||
| self.outstanding_item.visible = False | ||
|
|
||
| # Expiring contracts | ||
| if expiring_count > 0: | ||
| self.expiring_item.set_text(f"{expiring_count} ending soon") | ||
| self.expiring_item.visible = True | ||
| else: | ||
| self.expiring_item.visible = False | ||
|
|
||
| self._warning_divider.visible = has_warnings | ||
|
|
||
| # Update bar color based on urgency | ||
| if overdue_count > 0: | ||
| self.bar.bgcolor = colors.bg_statusbar_danger | ||
| elif has_warnings: | ||
| self.bar.bgcolor = colors.bg_statusbar_warning | ||
| else: | ||
| self.bar.bgcolor = colors.bg_statusbar | ||
|
|
There was a problem hiding this comment.
StatusBarManager.update_warnings() and try_update() assume self.bar exists, but self.bar is only created in build(). If update_warnings() is called before build(), this will raise AttributeError (and currently gets silently swallowed by callers). Consider initializing self.bar to None in __init__ and guarding these methods, or calling build() during construction so the manager is always safe to update.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| def update_value(self, new_value: str): | ||
| self.drop_down.value = new_value | ||
| self.drop_down.error_text = None | ||
|
|
There was a problem hiding this comment.
TDropDown.update_value() no longer calls update(), unlike update_dropdown_items() / update_error_txt(). Call sites like PreferencesScreen.refresh_preferences_items() use update_value() without a subsequent update_self(), so the UI may not refresh when the value is programmatically changed. Consider calling self.update() (or self.drop_down.update()) after setting the value when the control is mounted.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
| def set_date(self, date: Optional[datetime.date] = None): | ||
| if date is None: | ||
| return | ||
| self._selected_date = date | ||
| self._picker.value = datetime.datetime.combine(date, datetime.time()) | ||
| self._date_text.value = date.strftime(self._DATE_FMT) | ||
| self._date_text.color = colors.text_primary | ||
| self.update() | ||
|
|
There was a problem hiding this comment.
DateSelector.set_date() updates internal state and the displayed text, but it no longer calls update(). This can leave the UI stale when set_date() is called on an already-mounted control (e.g., date presets in invoicing or when loading an entity into an editor). Consider restoring self.update() (or updating the specific text control) after mutating the values.
There was a problem hiding this comment.
@copilot open a new pull request to apply changes based on this feedback
Co-authored-by: clstaudt <875194+clstaudt@users.noreply.github.com>
Use TextOverflow.ELLIPSIS enum instead of string literal in projects/view.py
Co-authored-by: clstaudt <875194+clstaudt@users.noreply.github.com>
Co-authored-by: clstaudt <875194+clstaudt@users.noreply.github.com>
…n mounted Co-authored-by: clstaudt <875194+clstaudt@users.noreply.github.com>
Guard StatusBarManager methods against pre-build AttributeError
Fix TDropDown.update_value() missing UI refresh call
Restore `self.update()` in `DateSelector.set_date()`
No description provided.