# Changelog 2026-06 [#2026-06] * **`@daypicker/react` v10**: Migrated from `react-day-picker` to `@daypicker/react`. Updated the shadcn `Calendar` wrapper for v10 (`month_grid` class name, Spanish locale). * **`DateRangeSelector`**: Rebuilt as a Popover + Calendar range picker with composable inline presets and custom date selection. Calendar range limited to the last 13 months through today. * **`/demo/finances` layout**: Split into **Análisis Financiero** (KPIs + stacked chart) and **Facturas** (invoice table), each with an independent date filter. * **`BillingTable`**: Invoices keyed by `emissionDate` (ISO `YYYY-MM-DD`) instead of monthly rows. TanStack Table `columnFilters` + `getFilteredRowModel` for client-side date filtering. Spanish formatting via `date-fns`. * **`date-fns` adoption**: Date logic standardized across the app — `startOfDay`, `endOfDay`, `subDays`, `subMonths`, `parseISO`, `isWithinInterval`, and `format` with the `es` locale replace manual `Date` manipulation and `Intl`/`toLocaleDateString` calls. # Billing Table Installation [#installation] Installing this item also copies `paginated-table.tsx`. Usage [#usage] Pass the full `billings` array. Date filtering is a TanStack column filter on `emissionDate`; the parent owns that state. Rows sort by `emissionDate` descending and paginate at 10 by default. ```jsx import { useState } from "react"; import { endOfDay, startOfDay, subDays } from "date-fns"; import { BillingTable } from "@/components/registry/billing-table"; import { DateRangeSelector } from "@/components/registry/date-range-picker"; export function Example() { const [columnFilters, setColumnFilters] = useState(() => { const to = endOfDay(new Date()); return [ { id: "emissionDate", value: { from: startOfDay(subDays(to, 29)), to }, }, ]; }); return ( <> filter.id === "emissionDate")?.value} onDateChange={(range) => setColumnFilters([{ id: "emissionDate", value: range }]) } presets={[ { value: "30days", label: "Último mes", daysBack: 29 }, { value: "90days", label: "Últimos 3 meses", daysBack: 89 }, { value: "365days", label: "Último año", daysBack: 364 }, ]} /> ); } ``` Display [#display] * Fecha de emisión: `format(parseISO(emissionDate), "PP", { locale: es })` * Estado: badge, "Pendiente" or "Enviada" * Importe: `es-ES` EUR * Factura: link labeled `Factura `, opens in a new tab The date-range filter uses `isWithinInterval` from `date-fns`. If `from` or `to` is missing, every row passes. API Reference [#api-reference] BillingTable [#billingtable] The `BillingTable` component wraps `PaginatedTable` with billing columns. | Prop | Type | Default | | ----------------------- | -------------------------------- | ------- | | `billings` | `BillingEntry[]` | - | | `columnFilters` | `ColumnFiltersState` | - | | `onColumnFiltersChange` | `OnChangeFn` | - | | `isPending` | `boolean` | `false` | BillingEntry [#billingentry] | Prop | Type | Default | | -------------- | --------------------- | ------- | | `emissionDate` | `string` | - | | `state` | `"pending" \| "sent"` | - | | `imports` | `number` | - | | `bill` | `string` | - | # Chart Area Installation [#installation] Usage [#usage] Each data item follows the `Order` shape. The chart only plots `delivered.amount` and `received.amount`. Axis and tooltip dates are formatted in Spanish with `date-fns`. Series labels are fixed: Entregas (`delivered`) and Devoluciones (`received`). ```jsx import { ChartArea } from "@/components/registry/chart-area"; export function Example() { const data = [ { date: new Date(), expired: 0, deliveryTime: 0, incidents: 0, toReceive: 0, toDeliver: 0, atPudo: 0, delivered: { amount: 25, ratioEuros: 0 }, received: { amount: 10, ratioEuros: 0 }, comissionSales: { amount: 0, ratioEuros: 0 }, shopToShop: { amount: 0, ratioEuros: 0 }, returns: 0, }, ]; return ; } ``` API Reference [#api-reference] ChartArea [#chartarea] The `ChartArea` component plots two series on an area chart. | Prop | Type | Default | | ------- | ---------- | ------- | | `title` | `string` | - | | `data` | `object[]` | - | data item [#data-item] | Prop | Type | Default | | ---------------- | ---------------------------------------- | ------- | | `date` | `Date` | - | | `delivered` | `{ amount: number; ratioEuros: number }` | - | | `received` | `{ amount: number; ratioEuros: number }` | - | | `expired` | `number` | - | | `deliveryTime` | `number` | - | | `incidents` | `number` | - | | `toReceive` | `number` | - | | `toDeliver` | `number` | - | | `atPudo` | `number` | - | | `comissionSales` | `{ amount: number; ratioEuros: number }` | - | | `shopToShop` | `{ amount: number; ratioEuros: number }` | - | | `returns` | `number` | - | # Daily Metrics Card Installation [#installation] Usage [#usage] Pass a Lucide icon component, not a React node. The card sizes the icon itself. ```jsx import { Package } from "lucide-react"; import { DailyMetricsCard } from "@/components/registry/daily-metrics-card"; export function Example() { return ; } ``` API Reference [#api-reference] DailyMetricsCard [#dailymetricscard] `DailyMetricsCard` renders one labeled metric. | Prop | Type | Default | | ------- | ------------ | ------- | | `label` | `string` | - | | `value` | `number` | - | | `icon` | `LucideIcon` | - | # Date Range Picker Installation [#installation] The component depends on `@daypicker/react` (v10) and the shadcn `Calendar` component. Usage [#usage] You supply the preset list. The component applies the selected preset or a custom range from the calendar. ```jsx import { useState } from "react"; import type { DateRange } from "@daypicker/react"; import { endOfDay, startOfDay, subDays } from "date-fns"; import { DateRangeSelector } from "@/components/registry/date-range-picker"; export function Example() { const [range, setRange] = useState(() => { const to = endOfDay(new Date()); return { from: startOfDay(subDays(to, 13)), to }; }); return ( ); } ``` Longer windows, for example billing: ```jsx ``` Behaviour [#behaviour] The trigger shows the selected range in Spanish (`es` via `date-fns`). Presets sit on the left of the popover, the two-month calendar on the right. Presets use `startOfDay` / `endOfDay`. Picking dates on the calendar clears the active preset. Dates after today, and more than 13 months back, are disabled. API Reference [#api-reference] DateRangeSelector [#daterangeselector] `DateRangeSelector` is the exported component from `date-range-picker`. | Prop | Type | Default | | --------------- | ----------------------------------------- | ------------------ | | `presets` | `DateRangePreset[]` | - | | `value` | `DateRange \| undefined` | - | | `onDateChange` | `(range: DateRange \| undefined) => void` | - | | `defaultPreset` | `string` | `presets[0].value` | DateRangePreset [#daterangepreset] | Prop | Type | Default | | ---------- | -------- | ------- | | `value` | `string` | - | | `label` | `string` | - | | `daysBack` | `number` | - | # Finances Chart Card Installation [#installation] Usage [#usage] The title is fixed as "Ingresos". `kpi.value` is drawn in the centre of the pie as euros. Slice values come from `earningsBreakdown`. The field is spelled `comissionSales` (one "m"), matching the rest of the registry. ```jsx import { FinancesChartCard } from "@/components/registry/finances-chart-card"; export function Example() { return ( ); } ``` API Reference [#api-reference] FinancesChartCard [#financeschartcard] The `FinancesChartCard` component shows total earnings and a category breakdown. | Prop | Type | Default | | ------------------- | -------- | ------- | | `kpi` | `object` | - | | `earningsBreakdown` | `object` | - | kpi [#kpi] | Prop | Type | Default | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | | `type` | `"sales" \| "receivedPackages" \| "deliveredPackages" \| "clientReturns" \| "incidents" \| "expiredPackages" \| "earnings" \| "commissionSales"` | - | | `value` | `number` | - | | `changePercentage` | `number` | - | | `unit` | `"EUR" \| "%" \| "package"` | - | earningsBreakdown [#earningsbreakdown] | Prop | Type | Default | | ---------------- | -------- | ------- | | `deliveries` | `number` | - | | `returns` | `number` | - | | `comissionSales` | `number` | - | | `shopToShop` | `number` | - | # KPI Card Installation [#installation] Usage [#usage] `type` picks the Spanish label and Lucide icon. For `incidents` and `expiredPackages`, a decrease is treated as favourable (green). `unit` is part of the payload but is not shown. `earnings` is in the type union but has no label or icon mapping, so it will render without either. ```jsx import { KPICard } from "@/components/registry/kpi-card"; export function Example() { return ( ); } ``` API Reference [#api-reference] KPICard [#kpicard] The `KPICard` component displays one KPI. | Prop | Type | Default | | ----- | -------- | ------- | | `kpi` | `object` | - | kpi [#kpi] | Prop | Type | Default | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | | `type` | `"sales" \| "receivedPackages" \| "deliveredPackages" \| "clientReturns" \| "incidents" \| "expiredPackages" \| "earnings" \| "commissionSales"` | - | | `value` | `number` | - | | `changePercentage` | `number` | - | | `unit` | `"EUR" \| "%" \| "package"` | - | # Label Requests List Installation [#installation] Usage [#usage] Pass the full `requests` array. Status filtering is owned inside the component (`all` or a `LabelRequestStatus`). Pending rows expose cancel / mark-received actions. Automatic pending requests underline “automáticamente” to open the Nota popover (auto-send below 20% remaining). ```jsx import { LabelRequestsList } from "@/components/registry/label-requests-list"; import { labelRequests } from "@/mocks"; export function Example() { return ( {}} onMarkReceived={(id) => {}} /> ); } ``` Display [#display] * Title: `Solicitud #{id}` * Pending automatic: “Creada automáticamente el …” with Nota popover * Pending manual: “Creada manualmente el …” * Cancelled / received: status badge + status subtitle (`updatedAt` via `PP` + `es`) Filters (UI labels in Spanish): Todas, Pendientes, Canceladas, Recibidas. Every request requires a `reason` (`few_labels` | `damaged` | `not_received`). API Reference [#api-reference] LabelRequestsList [#labelrequestslist] | Prop | Type | Default | | ---------------- | ---------------------- | ------- | | `requests` | `LabelRequest[]` | - | | `onCancel` | `(id: string) => void` | - | | `onMarkReceived` | `(id: string) => void` | - | LabelRequest [#labelrequest] | Prop | Type | Default | | ----------- | --------------------------------------------- | ------- | | `id` | `string` | - | | `status` | `"pending" \| "cancelled" \| "received"` | - | | `source` | `"automatic" \| "manual"` | - | | `updatedAt` | `string` | - | | `reason` | `"few_labels" \| "damaged" \| "not_received"` | - | # Labels List Installation [#installation] Usage [#usage] Pass the full `labels` array. Status filtering is owned inside the component (`all` or a `LabelStatus`). Wire `onMarkConsumed` for the trash action on active packs. ```jsx import { LabelsList } from "@/components/registry/labels-list"; import { labels } from "@/mocks"; export function Example() { return ( { // open confirm dialog for id }} /> ); } ``` Display [#display] * Range: `rangeFrom - rangeTo` * Subtitle from `status` + `updatedAt` (`format(..., "PP", { locale: es })`) * Active: percent and `used/500 usadas`, trash action * Consumed / disabled: status badge Filters (UI labels in Spanish): Todas, Activas, Consumidas, Desactivadas. Pack size is always 500. API Reference [#api-reference] LabelsList [#labelslist] | Prop | Type | Default | | ---------------- | ---------------------- | ------- | | `labels` | `LabelPack[]` | - | | `onMarkConsumed` | `(id: string) => void` | - | LabelPack [#labelpack] | Prop | Type | Default | | ----------- | -------------------------------------- | ------- | | `id` | `string` | - | | `rangeFrom` | `string` | - | | `rangeTo` | `string` | - | | `status` | `"active" \| "consumed" \| "disabled"` | - | | `updatedAt` | `string` | - | | `used` | `number` | - | # Labels Table Installation [#installation] Installing this item also copies `paginated-table.tsx` and `pudo-selector.tsx`. Usage [#usage] Pass the full `labels` array. Search, point, and status filters are owned inside the component. Wire `onDeactivate` and `onActivate` for the row actions. ```jsx import { LabelsTable } from "@/components/registry/labels-table"; import { labelTableRows } from "@/mocks"; export function Example() { return ( { // open confirm dialog for id }} onActivate={(id) => { // open confirm dialog for id }} /> ); } ``` Display [#display] * Rango: `rangeFrom - rangeTo` * Parcel Shop: `pudo.name` and `pudo.lineAddress1` * Uso: percent and `used/500` (pack size is always 500) * Estado: Activo, Consumido, or Desactivado * Última modificación: `updatedAt` as `dd/MM/yy` * Código postal: `pudo.postalCode` * Actions: **Desactivar etiquetas** on active rows, **Activar etiquetas** on disabled rows. Consumed rows show a disabled **Desactivar etiquetas** button Filters (UI labels in Spanish): search by etiqueta or postal code, **Punto**, **Estado** (Activas, Consumidas, Desactivadas). Filtering and pagination are client-side. API Reference [#api-reference] LabelsTable [#labelstable] | Prop | Type | Default | | -------------- | ---------------------- | ------- | | `labels` | `LabelTableRow[]` | - | | `onDeactivate` | `(id: string) => void` | - | | `onActivate` | `(id: string) => void` | - | | `isPending` | `boolean` | `false` | LabelTableRow [#labeltablerow] | Prop | Type | Default | | ----------- | -------------------------------------- | ------- | | `id` | `string` | - | | `rangeFrom` | `string` | - | | `rangeTo` | `string` | - | | `status` | `"active" \| "consumed" \| "disabled"` | - | | `updatedAt` | `string` | - | | `used` | `number` | - | | `pudo` | `PudoPoint` | - | # Paginated Table Installation [#installation] Usage [#usage] Build the table with `useReactTable` and pass the instance in. Page size options are 1, 3, 5, 10, and 100. Columns with id `select` or `actions` render narrower than the rest. ```jsx import { getCoreRowModel, getPaginationRowModel, useReactTable, } from "@tanstack/react-table"; import PaginatedTable from "@/components/registry/paginated-table"; const columns = [{ accessorKey: "name", header: "Name" }]; export function Example() { const table = useReactTable({ data: [{ name: "Alice" }, { name: "Bob" }], columns, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), }); return ; } ``` Keyboard shortcuts [#keyboard-shortcuts] * Left / Right: previous / next page * Home / End: first / last page API Reference [#api-reference] PaginatedTable [#paginatedtable] `PaginatedTable` is a default export. The empty-state copy is Spanish. | Prop | Type | Default | | ----------- | ---------- | ------- | | `table` | `Table` | - | | `empty` | `object` | - | | `isPending` | `boolean` | - | empty [#empty] | Prop | Type | Default | | ------------- | ----------- | ------------------------------------------------------------- | | `title` | `string` | `"No se encontraron resultados"` | | `description` | `string` | `"Intenta ajustar los filtros o realizar una nueva búsqueda"` | | `icon` | `ReactNode` | `` | # Pudo Selector Installation [#installation] Usage [#usage] Pass the full `pudos` list. The parent owns the selected ids. ```jsx import { useState } from "react"; import { PudoSelector } from "@/components/registry/pudo-selector"; import { pudos } from "@/mocks"; export function Example() { const [selectedIds, setSelectedIds] = useState([]); return ( setSelectedIds([])} /> ); } ``` Behaviour [#behaviour] The trigger shows **Punto** and a count badge when something is selected. The popover searches by name and address (`lineAddress1`). **Restablecer** clears the selection. API Reference [#api-reference] PudoSelector [#pudoselector] | Prop | Type | Default | | ------------- | ------------------------- | ------- | | `pudos` | `PudoPoint[]` | - | | `selectedIds` | `string[]` | `[]` | | `onToggle` | `(ids: string[]) => void` | - | | `onClear` | `() => void` | - | | `className` | `string` | - | | `disabled` | `boolean` | `false` | PudoPoint [#pudopoint] | Prop | Type | Default | | -------------- | -------- | ------- | | `id` | `string` | - | | `name` | `string` | - | | `lineAddress1` | `string` | - | | `postalCode` | `string` | - | # Getting Started This documentation explains how to install the React UI components provided by ecodeliver. Context [#context] In the UI ecosystem, there are many different opinions and approaches to component development. Traditional UI component libraries are often difficult to customize, while building everything from scratch can be extremely time-consuming. As a result, most solutions tend to fall on opposite ends of the spectrum: * On one side, there are the “build everything yourself” solutions, where flexibility is the priority, but very little is provided out of the box, leaving most implementation details up to you. * On the other side, there are the “fully prebuilt” solutions, which provide everything upfront but often sacrifice flexibility, making it difficult to adapt components to specific edge cases or fit them naturally into your product’s UI. The solution we provide aims to strike a balance between these two approaches: components that are already built and production-ready, while still remaining fully customizable and easy to adapt to your needs. This is where [shadcn/ui](https://ui.shadcn.com/) comes in. shadcn is not a traditional component library, but rather a system for distributing code and components directly into your codebase. We highly encourage you to read [their introduction](https://ui.shadcn.com/docs) to better understand the philosophy behind it. This technology is compatible with other ui libraries like materialUI. What to expect from this deliverable [#what-to-expect-from-this-deliverable] * 7 custom-built components * A CLI-based installation flow, so the component source code lives directly in your codebase Continue with [Installation](/docs/installation) to configure your project and install components. # Installation Requirements [#requirements] To install the components, your project must meet the following requirements: 1. [Tailwind CSS](https://tailwindcss.com/) — The components use Tailwind for styling. Refer to the [official installation guide](https://tailwindcss.com/docs/installation/using-vite) for setup instructions. 2. [Import aliases](https://ui.shadcn.com/docs/package-imports) — This allows us to avoid making assumptions about how your project is structured or how your imports are organized. 3. [@daypicker/react](https://daypicker.dev) (v10) and [date-fns](https://date-fns.org/) — Required by the date range picker, calendar, billing table, and chart components for range selection, parsing, filtering, and Spanish date formatting. If you encounter any issues or blockers while configuring these requirements, let us know and we’ll be happy to help. A note on Tailwind CSS installation [#a-note-on-tailwind-css-installation] Tailwind's default installations come with something called [Preflight](https://tailwindcss.com/docs/preflight), a set of base styles that are designed to smooth over cross-browser inconsistencies and make it easier for you to work within the constraints of your design system. If you have an existing project with other component libraries, this might break some existing styling behaviours. If you do not want to change your base styles according to Tailwind's standards, follow [the official guide to disable Preflight](https://tailwindcss.com/docs/preflight#disabling-preflight). A note on the import aliases guide [#a-note-on-the-import-aliases-guide] The shadcn [official guide](https://ui.shadcn.com/docs/package-imports) assumes TypeScript usage and configuration through `tsconfig.json`. If your project uses JavaScript instead, the following approach may be more appropriate: 1. Create a `jsconfig.json` in case you didn’t have it and add the following configuration. The following example assumes your source code is located in the `src` directory, but feel free to change this however your project might be configured: ```json { "compilerOptions": { "paths": { "@/*": [ "./src/*" ] } } } ``` If you are using Vite, you will also need to configure alias resolution there. A typical configuration would look like this: ```javascript import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' import path from 'node:path' export default defineConfig({ plugins: [react(), tailwindcss()], resolve: { alias: { '@': path.resolve(__dirname, './src'), }, }, }) ``` Installation instructions [#installation-instructions] Once you have completed the previous steps, run the following command: ```bash # Initializes shadcn npx shadcn@latest init --preset b1Z5d0f9U ``` You’ll notice the `--preset` flag. This preset has already been configured for you, but you are free to customize it however you see fit using the [shadcn preset editor](https://ui.shadcn.com/create?preset=b1Z5d0f9U\&template=vite). Through this interface, you can adjust colors, fonts, spacing, border radius values, and other UI aspects. Once you are satisfied with the preset, run the command above to install all required configuration files and dependencies. After completing this step, you’ll be ready to use any of the [shadcn components](https://ui.shadcn.com/docs/components) or [blocks](https://ui.shadcn.com/blocks). However, the custom components we provide require one final configuration step. One of the files generated by the shadcn initialization process is `components.json`, which contains information about your current project setup. In this file, you will need to add our [custom registry](https://ui.shadcn.com/docs/registry) configuration. ```json { "$schema": "https://ui.shadcn.com/schema.json", "style": "radix-vega", "rsc": false, "tsx": false, "tailwind": { "config": "", "css": "src/index.css", "baseColor": "stone", "cssVariables": true, "prefix": "" }, "iconLibrary": "lucide", "rtl": false, "aliases": { "components": "@/components", "utils": "@/lib/utils", "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" }, "menuColor": "default", "menuAccent": "subtle", "registries": { "@ecodeliver": "https://gls-component-registry.ecodeliver.tech/r/{name}.json" } } ``` Please note that depending on what preset you've used for the installation this configuration might look a little differently. The key change you must do after the initialization step is adding the ecodeliver registry. This will tell shadcn how to use the `@ecodeliver` component registry, enabling installation of our custom components. You can install them like this: ```bash # Adds just one component npx shadcn@latest add @ecodeliver/daily-metrics-card # Adds all components npx shadcn@latest add @ecodeliver/billing-table @ecodeliver/chart-area @ecodeliver/daily-metrics-card @ecodeliver/date-range-picker @ecodeliver/finances-chart-card @ecodeliver/kpi-card @ecodeliver/paginated-table ``` These components will now live directly in your codebase, and you are free to use and customize them however you like.