Skip to content

Streamline UI/UX for version 2.1#239

Merged
clstaudt merged 16 commits intomainfrom
streamline
Mar 12, 2026
Merged

Streamline UI/UX for version 2.1#239
clstaudt merged 16 commits intomainfrom
streamline

Conversation

@clstaudt
Copy link
Contributor

No description provided.

clstaudt added 2 commits March 8, 2026 10:11
- 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.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_bar Container. As a result the manager's build()/try_update() never affects anything visible. Replace the static status bar with self.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.

Comment on lines +100 to +107
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,
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Comment on lines +103 to +111
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,
),
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +251 to +290
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

Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Comment on lines +448 to +451
def update_value(self, new_value: str):
self.drop_down.value = new_value
self.drop_down.error_text = None

Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Comment on lines 567 to 574
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()

Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Copy link
Contributor

Copilot AI commented Mar 10, 2026

@clstaudt I've opened a new pull request, #240, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI and others added 3 commits March 10, 2026 07:36
Co-authored-by: clstaudt <875194+clstaudt@users.noreply.github.com>
Use TextOverflow.ELLIPSIS enum instead of string literal in projects/view.py
Copy link
Contributor

Copilot AI commented Mar 10, 2026

@clstaudt I've opened a new pull request, #241, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Contributor

Copilot AI commented Mar 10, 2026

@clstaudt I've opened a new pull request, #242, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Contributor

Copilot AI commented Mar 10, 2026

@clstaudt I've opened a new pull request, #243, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI and others added 7 commits March 10, 2026 09:02
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()`
@clstaudt clstaudt merged commit b427374 into main Mar 12, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants