Skip to content

feat: shared UI components and Cally date pickers#140

Merged
emilwojtaszek merged 3 commits into
mainfrom
feat/shared-ui-components-and-date-pickers
Apr 11, 2026
Merged

feat: shared UI components and Cally date pickers#140
emilwojtaszek merged 3 commits into
mainfrom
feat/shared-ui-components-and-date-pickers

Conversation

@emilwojtaszek

@emilwojtaszek emilwojtaszek commented Apr 10, 2026

Copy link
Copy Markdown
Member

Summary

  • Replace native <input type="date"> and <input type="month"> with custom calendar components powered by the Cally web component library, styled automatically by daisyUI's .cally class
  • Extract 5 duplicated UI patterns into shared CoreComponents, eliminating ~100 lines of copy-pasted HTML across 15+ LiveView files
  • Add component-first development guidelines to CLAUDE.md and ADR 0043

New shared components

Component Purpose Replaces
date_range_picker Cally calendar-range in dropdown (sm + default sizes) Paired <input type="date"> on 4 pages
date_picker Cally calendar-date in dropdown <input type="date"> on invoice edit form
month_range_picker Custom month grid with year navigation <input type="month"> on billing period
table_container Bordered scrollable table wrapper 9 copy-pasted div wrappers
empty_state Centered "no results" message 5 identical paragraphs
search_input Icon + text input for filter bars 2 duplicated blocks
reset_filters_button Filter clear button 3 identical buttons
outline-destructive Button variant for delete/destructive actions 6 inline class overrides

Pages updated

Dashboard, Invoice list, Invoice show, Export, Payment requests, Categories, Tokens, Bank accounts, Sync, Team, Company list, Company form

Test plan

  • mix compile --warnings-as-errors — clean
  • mix test — 1855 tests pass, 0 failures
  • mix format --check-formatted — clean
  • mix credo --strict — no issues
  • Manual: verify date range picker on dashboard, invoice list, payment requests
  • Manual: verify date range picker (full-width) on export page
  • Manual: verify single date picker on invoice edit form
  • Manual: verify month range picker on billing period edit
  • Manual: verify "today" indicator is visually distinct from selected dates

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Calendar date picker, date range picker, and month-range picker with popover interactions.
    • Reusable UI components: search input, reset-filters button, table container, and empty-state display.
    • New "outline-destructive" button variant for destructive actions.
  • Styling

    • Updated calendar "today" and selected-day visuals for clearer emphasis.
  • Documentation

    • Added component-driven UI guidance and an ADR specifying component conventions.

…ally

Replace native <input type="date"> and <input type="month"> with custom
calendar components powered by the Cally web component library, styled
by daisyUI's .cally class.

New shared components in CoreComponents:
- date_range_picker: Cally calendar-range in a dropdown (sm/default sizes)
- date_picker: Cally calendar-date in a dropdown for single dates
- month_range_picker: custom month grid with year nav for billing periods
- table_container: bordered scrollable table wrapper (9 usages)
- empty_state: centered "no results" message (5 usages)
- search_input: magnifying glass icon + text input (2 usages)
- reset_filters_button: filter clear button (3 usages)
- outline-destructive button variant (6 usages)

Migrated all LiveView templates to use shared components, eliminating
~100 lines of duplicated HTML. Added ADR 0043 and CLAUDE.md guidelines
for component-first development.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 10, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ef570e47-224a-43f6-818b-b0dde6820be5

📥 Commits

Reviewing files that changed from the base of the PR and between ce2f2e8 and b5a59cb.

📒 Files selected for processing (1)
  • assets/js/app.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • assets/js/app.js

📝 Walkthrough

Walkthrough

Adds a component-driven UI: ADR and docs, CoreComponents (table container, empty state, search/reset, date/month pickers, button variant), a vendored Cally calendar web-component, LiveSocket hooks for calendar popovers, CSS overrides for calendar styling, and many LiveView templates updated to use the new components.

Changes

Cohort / File(s) Summary
Docs & ADR
CLAUDE.md, docs/adr/0043-component-driven-ui.md
Adds component-first LiveView guidance and ADR 0043 defining extraction threshold, placement rules, and conventions (doc/spec/attr/slot/class/rest).
Vendored calendar web-component
assets/vendor/cally.js
Introduces a self-contained calendar runtime and custom elements (calendar-date, calendar-range, calendar-month, calendar-multi, selectors) with UTC date primitives, rendering/runtime, events, and exported constructors.
Client hooks & app JS
assets/js/app.js
Imports vendored cally.js; registers LiveSocket hooks DateRangePicker, DatePicker, MonthRangePicker; adds shared popover/calendar hook logic, selection parsing, hidden-input population, outside-click dismissal, and month-range UI behavior.
Calendar styling
assets/css/app.css
Overrides Cally shadow parts to change “today” underline styling and restore filled appearance when a day is both selected and today.
CoreComponents implementation
lib/ksef_hub_web/components/core_components.ex
Adds components: table_container, empty_state, search_input, reset_filters_button, date_range_picker, date_picker, month_range_picker; adds outline-destructive button variant and mapping to CSS classes.
LiveView templates — containers & actions
lib/ksef_hub_web/live/...
bank_account_live.ex, category_live/index.ex, company_live/index.ex, sync_live.ex, token_live.ex, team_live.ex, team_member_live/show.ex
Replaces manual bordered/overflow wrappers with <.table_container>, replaces empty-state paragraphs with <.empty_state>, and swaps destructive buttons to variant="outline-destructive".
LiveView templates — filters/search/date inputs
lib/ksef_hub_web/live/...
dashboard_live.ex, export_live/index.ex, invoice_live/index.ex, invoice_live/show.ex, payment_request_live/index.ex, company_live/form.ex
Replaces paired date/month inputs with date_range_picker/date_picker/month_range_picker, swaps inline search/reset to search_input/reset_filters_button, and removes a now-unused private month-format helper.
Tests
test/ksef_hub_web/live/invoice_live/show_test.exs
Updates selector assertions from input#id to #id to reflect component-wrapped inputs.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Trigger as Trigger/Popover
    participant Cally as CallyCalendar
    participant Hook as LiveView Hook
    participant Inputs as Hidden Form Inputs

    User->>Trigger: Click trigger
    Trigger->>Trigger: Toggle popover (show calendar)
    User->>Cally: Select date(s) or month(s)
    Cally->>Hook: Emit "change" event with value
    Hook->>Hook: Parse value (single / range / month-range)
    Hook->>Inputs: Populate hidden inputs (data-from / data-to / data-value)
    Hook->>Inputs: Dispatch bubbling "input" event(s)
    Hook->>Trigger: Hide popover
    Note over Hook: destroyed(): remove outside-click listener
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰
I hopped a patch from date to view,
Tables trimmed and components grew.
Calendars pop where inputs hide,
Filters tidy, reset at side.
Reusable carrots for code—hip hop, anew!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.05% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the two main changes: introducing shared UI components and integrating Cally date pickers throughout the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/shared-ui-components-and-date-pickers

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (5)
assets/js/app.js (1)

210-219: Consider dispatching input event on both hidden inputs.

Currently only fromInput dispatches the input event. While this works for triggering a single phx-change, if form validation or processing depends on individual field change events, the toInput change may be missed.

💡 Optional: dispatch on both inputs
 const DateRangePicker = calendarPickerHook("calendar-range", (hook, value) => {
   if (value && value.includes("/")) {
     const [from, to] = value.split("/")
     const fromInput = hook.el.querySelector("[data-from]")
     const toInput = hook.el.querySelector("[data-to]")
     fromInput.value = from
     toInput.value = to
     fromInput.dispatchEvent(new Event("input", { bubbles: true }))
+    toInput.dispatchEvent(new Event("input", { bubbles: true }))
   }
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/js/app.js` around lines 210 - 219, The DateRangePicker callback
currently parses value into from/to and only dispatches an "input" Event on
fromInput; update the handler in the calendarPickerHook callback
(DateRangePicker) to also dispatch an "input" Event on toInput after setting its
value so both hidden inputs emit input events (fromInput and toInput) to ensure
any per-field listeners or phx-change behavior run for the "to" field as well.
lib/ksef_hub_web/components/core_components.ex (3)

717-729: Consider adding @rest for HTML attribute forwarding.

Similar to table_container, this component would benefit from @rest support for attributes like data-testid.

♻️ Add `@rest` support
   attr :icon, :string, default: nil
   attr :class, :string, default: nil
+  attr :rest, :global
   slot :inner_block, required: true

   `@spec` empty_state(map()) :: Phoenix.LiveView.Rendered.t()
   def empty_state(assigns) do
     ~H"""
-    <div class={["text-center text-muted-foreground py-8", `@class`]}>
+    <div class={["text-center text-muted-foreground py-8", `@class`]} {`@rest`}>
       <.icon :if={`@icon`} name={`@icon`} class="size-8 mx-auto mb-2 opacity-40" />
       <p>{render_slot(`@inner_block`)}</p>
     </div>
     """
   end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ksef_hub_web/components/core_components.ex` around lines 717 - 729, The
empty_state component lacks HTML attribute forwarding; add a global rest attr
and pass it into the container div so callers can supply arbitrary HTML
attributes (e.g., data-testid). Concretely, add attr :rest, :global to the
component attrs (alongside :icon and :class) and include {`@rest`} on the root
<div> in empty_state (similar to how table_container handles `@rest`) so
attributes are forwarded to the rendered element.

695-707: Consider adding @rest for HTML attribute forwarding.

The table_container component follows the required conventions (@doc, @spec, attr, slot), but lacks @rest for forwarding arbitrary HTML attributes like data-testid. As per coding guidelines, components should use @rest (:global) to forward HTML attributes.

♻️ Add `@rest` support
   attr :class, :string, default: nil
+  attr :rest, :global
   slot :inner_block, required: true

   `@spec` table_container(map()) :: Phoenix.LiveView.Rendered.t()
   def table_container(assigns) do
     ~H"""
-    <div class={["rounded-lg border border-border overflow-hidden", `@class`]}>
+    <div class={["rounded-lg border border-border overflow-hidden", `@class`]} {`@rest`}>
       <div class="overflow-x-auto">
         {render_slot(`@inner_block`)}
       </div>
     </div>
     """
   end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ksef_hub_web/components/core_components.ex` around lines 695 - 707, The
component table_container is missing HTML attribute forwarding; add an attr
:rest, :global to the component (alongside the existing attr :class and slot
:inner_block) and spread `@rest` onto the root container element inside the HEEx
template (e.g., include {...@rest} on the outer <div> that currently renders
class={["rounded-lg border border-border overflow-hidden", `@class`]} ) so
arbitrary attributes like data-testid are forwarded.

1264-1269: Add defensive handling for malformed input in format_month_label.

The String.split(ym, "-") pattern match assumes exactly two parts. Malformed input could cause a MatchError. Since this is called with output from date_to_month which validates format, the risk is low, but defensive coding would be safer.

🛡️ Defensive pattern match
   `@spec` format_month_label(String.t()) :: String.t()
   defp format_month_label(ym) when is_binary(ym) do
-    [year, month_str] = String.split(ym, "-")
-    {month_num, _} = Integer.parse(month_str)
-    "#{Enum.at(`@months`, month_num - 1)} #{year}"
+    case String.split(ym, "-") do
+      [year, month_str] ->
+        {month_num, _} = Integer.parse(month_str)
+        "#{Enum.at(`@months`, month_num - 1)} #{year}"
+
+      _ ->
+        ym
+    end
   end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/ksef_hub_web/components/core_components.ex` around lines 1264 - 1269,
format_month_label currently assumes ym is a binary "YYYY-MM" and will crash on
malformed input; update format_month_label to defensively handle bad input by
using String.split(ym, "-", parts: 2) and pattern-matching the result (e.g.,
case [year, month_str] -> parse month with Integer.parse(month_str) and if
successful return "#{Enum.at(`@months`, month_num - 1)} #{year}" else fall back to
returning ym or a safe default; for non-binary inputs add a clause or guard to
return a safe default), ensuring you reference format_month_label and `@months` in
the change.
assets/vendor/cally.js (1)

1-31: Add version and license header to vendored library.

The vendored cally.js file lacks version information and license attribution. This makes it difficult to track updates and ensure license compliance. Consider adding a header comment with version, source URL, and license.

📝 Suggested header
+/**
+ * Cally - Calendar Web Components
+ * `@version` [VERSION]
+ * `@source` https://github.com/WickyNilliams/cally
+ * `@license` MIT
+ */
 class le {
What is the current version of the Cally calendar library by WickyNilliams?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/vendor/cally.js` around lines 1 - 31, Add a header comment to the top
of assets/vendor/cally.js (the vendored library that defines class le with its
current/getter/setter and on method) containing the Cally library version,
source URL (upstream repo or npm page), copyright/author attribution
(WickyNilliams), and the license text or an SPDX identifier (e.g., MIT) so
license compliance and update tracking are clear; place the header as a block
comment above the class definition and include a brief "Vendored from" note and
a link to the original source.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@assets/js/app.js`:
- Around line 210-219: The DateRangePicker callback currently parses value into
from/to and only dispatches an "input" Event on fromInput; update the handler in
the calendarPickerHook callback (DateRangePicker) to also dispatch an "input"
Event on toInput after setting its value so both hidden inputs emit input events
(fromInput and toInput) to ensure any per-field listeners or phx-change behavior
run for the "to" field as well.

In `@assets/vendor/cally.js`:
- Around line 1-31: Add a header comment to the top of assets/vendor/cally.js
(the vendored library that defines class le with its current/getter/setter and
on method) containing the Cally library version, source URL (upstream repo or
npm page), copyright/author attribution (WickyNilliams), and the license text or
an SPDX identifier (e.g., MIT) so license compliance and update tracking are
clear; place the header as a block comment above the class definition and
include a brief "Vendored from" note and a link to the original source.

In `@lib/ksef_hub_web/components/core_components.ex`:
- Around line 717-729: The empty_state component lacks HTML attribute
forwarding; add a global rest attr and pass it into the container div so callers
can supply arbitrary HTML attributes (e.g., data-testid). Concretely, add attr
:rest, :global to the component attrs (alongside :icon and :class) and include
{`@rest`} on the root <div> in empty_state (similar to how table_container handles
`@rest`) so attributes are forwarded to the rendered element.
- Around line 695-707: The component table_container is missing HTML attribute
forwarding; add an attr :rest, :global to the component (alongside the existing
attr :class and slot :inner_block) and spread `@rest` onto the root container
element inside the HEEx template (e.g., include {...@rest} on the outer <div>
that currently renders class={["rounded-lg border border-border
overflow-hidden", `@class`]} ) so arbitrary attributes like data-testid are
forwarded.
- Around line 1264-1269: format_month_label currently assumes ym is a binary
"YYYY-MM" and will crash on malformed input; update format_month_label to
defensively handle bad input by using String.split(ym, "-", parts: 2) and
pattern-matching the result (e.g., case [year, month_str] -> parse month with
Integer.parse(month_str) and if successful return "#{Enum.at(`@months`, month_num
- 1)} #{year}" else fall back to returning ym or a safe default; for non-binary
inputs add a clause or guard to return a safe default), ensuring you reference
format_month_label and `@months` in the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2606a66c-10fe-4448-bba8-3dd9ed1062f0

📥 Commits

Reviewing files that changed from the base of the PR and between 909e743 and a9a661f.

📒 Files selected for processing (20)
  • CLAUDE.md
  • assets/css/app.css
  • assets/js/app.js
  • assets/vendor/cally.js
  • docs/adr/0043-component-driven-ui.md
  • lib/ksef_hub_web/components/core_components.ex
  • lib/ksef_hub_web/live/bank_account_live.ex
  • lib/ksef_hub_web/live/category_live/index.ex
  • lib/ksef_hub_web/live/company_live/form.ex
  • lib/ksef_hub_web/live/company_live/index.ex
  • lib/ksef_hub_web/live/dashboard_live.ex
  • lib/ksef_hub_web/live/export_live/index.ex
  • lib/ksef_hub_web/live/invoice_live/index.ex
  • lib/ksef_hub_web/live/invoice_live/show.ex
  • lib/ksef_hub_web/live/payment_request_live/index.ex
  • lib/ksef_hub_web/live/sync_live.ex
  • lib/ksef_hub_web/live/team_live.ex
  • lib/ksef_hub_web/live/team_member_live/show.ex
  • lib/ksef_hub_web/live/token_live.ex
  • test/ksef_hub_web/live/invoice_live/show_test.exs

- Dispatch input event on both from and to hidden inputs in
  DateRangePicker so per-field listeners fire for both
- Add vendor header to cally.js with version, license, source URL
- Add attr :rest, :global to table_container and empty_state for
  HTML attribute forwarding (e.g. data-testid)
- Make format_month_label defensive against malformed input

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
assets/js/app.js (2)

240-240: Specify radix in parseInt call.

Omitting the radix can lead to unexpected parsing behavior in edge cases. Explicitly passing 10 is a best practice.

♻️ Proposed fix
-    this.year = parseInt(this.yearEl.textContent)
+    this.year = parseInt(this.yearEl.textContent, 10)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/js/app.js` at line 240, The parseInt call that sets this.year from
this.yearEl.textContent should explicitly specify the radix to avoid ambiguous
parsing; update the expression in the constructor/initializer where this.year =
parseInt(this.yearEl.textContent) to pass 10 as the second argument (e.g.,
parseInt(..., 10)) so it reliably parses decimal years.

181-184: Consider adding null checks for queried elements.

If the component markup doesn't render expected selectors ([data-trigger], [data-popover], or the calendar element), the code will throw when calling addEventListener on null. Adding defensive checks or early returns would improve robustness.

🛡️ Suggested defensive check
     mounted() {
       this.calendar = this.el.querySelector(calendarSelector)
       this.trigger = this.el.querySelector("[data-trigger]")
       this.popover = this.el.querySelector("[data-popover]")
+
+      if (!this.calendar || !this.trigger || !this.popover) {
+        console.warn(`${calendarSelector} picker: missing required elements`)
+        return
+      }

       this.trigger.addEventListener("click", (e) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/js/app.js` around lines 181 - 184, The mounted() method currently
assigns this.calendar, this.trigger, and this.popover via this.el.querySelector
without null checks; update mounted() to defensively verify each queried element
(using calendarSelector and selectors "[data-trigger]" and "[data-popover]") and
either early-return or skip wiring event listeners when any of those are null so
calls like addEventListener never run on null; ensure subsequent code that
references this.calendar/this.trigger/this.popover checks existence or is only
executed after these guards.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@assets/js/app.js`:
- Around line 279-287: The MonthRangePicker commit block sets
this.fromInput.value and this.toInput.value but only dispatches an "input" event
for this.fromInput, which can stop per-field listeners on the to input from
running; update the commit sequence in the MonthRangePicker handler (the same
area that calls this.highlightMonths(), this.updateLabel(), and
this.popover.classList.add("hidden")) to also dispatch new Event("input", {
bubbles: true }) on this.toInput after setting its value so both hidden inputs
fire their input listeners.

---

Nitpick comments:
In `@assets/js/app.js`:
- Line 240: The parseInt call that sets this.year from this.yearEl.textContent
should explicitly specify the radix to avoid ambiguous parsing; update the
expression in the constructor/initializer where this.year =
parseInt(this.yearEl.textContent) to pass 10 as the second argument (e.g.,
parseInt(..., 10)) so it reliably parses decimal years.
- Around line 181-184: The mounted() method currently assigns this.calendar,
this.trigger, and this.popover via this.el.querySelector without null checks;
update mounted() to defensively verify each queried element (using
calendarSelector and selectors "[data-trigger]" and "[data-popover]") and either
early-return or skip wiring event listeners when any of those are null so calls
like addEventListener never run on null; ensure subsequent code that references
this.calendar/this.trigger/this.popover checks existence or is only executed
after these guards.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5f3a9d76-df9b-4a05-842c-b5603918cbb5

📥 Commits

Reviewing files that changed from the base of the PR and between a9a661f and ce2f2e8.

📒 Files selected for processing (3)
  • assets/js/app.js
  • assets/vendor/cally.js
  • lib/ksef_hub_web/components/core_components.ex

Comment thread assets/js/app.js
Also add explicit radix to parseInt for year parsing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@emilwojtaszek emilwojtaszek merged commit e1a27e7 into main Apr 11, 2026
3 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.

1 participant