{"id":35489063,"url":"https://github.com/white-wind-llc/table","last_synced_at":"2026-02-28T18:03:36.139Z","repository":{"id":315686238,"uuid":"1060459528","full_name":"White-Wind-LLC/table","owner":"White-Wind-LLC","description":"Data Table M3 for Compose Multiplatform","archived":false,"fork":false,"pushed_at":"2026-01-21T00:43:35.000Z","size":794,"stargazers_count":21,"open_issues_count":1,"forks_count":2,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-21T11:17:18.966Z","etag":null,"topics":["compose-multiplatform","kotlin-multiplatform","material3","table"],"latest_commit_sha":null,"homepage":"https://white-wind-llc.github.io/table/","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/White-Wind-LLC.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-20T00:40:26.000Z","updated_at":"2026-01-21T00:43:05.000Z","dependencies_parsed_at":"2025-09-20T04:23:29.140Z","dependency_job_id":"f1b136bf-1545-40a4-8231-a74b34bc1ddb","html_url":"https://github.com/White-Wind-LLC/table","commit_stats":null,"previous_names":["white-wind-llc/table"],"tags_count":36,"template":false,"template_full_name":null,"purl":"pkg:github/White-Wind-LLC/table","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/White-Wind-LLC%2Ftable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/White-Wind-LLC%2Ftable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/White-Wind-LLC%2Ftable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/White-Wind-LLC%2Ftable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/White-Wind-LLC","download_url":"https://codeload.github.com/White-Wind-LLC/table/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/White-Wind-LLC%2Ftable/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29590685,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-18T18:54:29.675Z","status":"ssl_error","status_checked_at":"2026-02-18T18:50:50.517Z","response_time":162,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["compose-multiplatform","kotlin-multiplatform","material3","table"],"created_at":"2026-01-03T15:16:36.500Z","updated_at":"2026-02-28T18:03:36.109Z","avatar_url":"https://github.com/White-Wind-LLC.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"### Data Table for Compose Multiplatform (Material 3)\n\n[![Maven Central](https://img.shields.io/maven-central/v/ua.wwind.table-kmp/table-core)](https://central.sonatype.com/artifact/ua.wwind.table-kmp/table-core)\n\nCompose Multiplatform data table with Material 3 look \u0026 feel. Includes a core table (`table-core`), a conditional\nformatting add‑on (`table-format`), and paging integration (`table-paging`).\n\n### Table of Contents\n\n- [Example](#example)\n- [Modules](#modules)\n- [Key features](#key-features)\n- [Installation](#installation)\n- [Compatibility](#compatibility)\n- [Quick start](#quick-start)\n- [Cell editing mode](#cell-editing-mode)\n- [Data grouping](#data-grouping)\n- [Footer row](#footer-row)\n- [Paging integration (table-paging)](#paging-integration-table-paging)\n- [Conditional formatting (table-format)](#conditional-formatting-table-format)\n- [Core API reference (table-core)](#core-api-reference-table-core)\n- [Filters (built‑in types)](#filters-builtin-types)\n- [Fast Filters](#fast-filters)\n- [Selection](#selection)\n- [Checkbox selection with tableData](#checkbox-selection-with-tabledata)\n- [Dynamic row height and auto‑width](#dynamic-row-height-and-autowidth)\n- [Drag-to-scroll](#drag-to-scroll)\n- [Custom header icons](#custom-header-icons)\n- [Supported targets](#supported-targets)\n- [Third-Party Libraries](#third-party-libraries)\n- [License](#license)\n\n### Example\n\nHere's what the data table looks like in action:\n\n![Data Table Example](docs/images/datatable-example.png)\nLive demo: [white-wind-llc.github.io/table](https://white-wind-llc.github.io/table/)\n\n### Modules\n\n- `table-core`: core table (rendering, header, sorting, column resize and reordering, filtering, row selection, i18n,\n  styling/customization; dynamic or fixed row height).\n- `table-format`: dialog and APIs for rule‑based conditional formatting for cells/rows.\n- `table-paging`: adapter on top of the core table for `PagingData` (`ua.wwind.paging`).\n\n### Key features\n\n- Header with sort/filter icons (customizable via `TableHeaderDefaults.icons`).\n- Per‑column sorting (3‑state: ASC → DESC → none).\n- Data grouping by column with customizable group headers and sticky positioning.\n- Footer row with customizable content per column (totals, averages, summaries); supports pinned and scrollable modes.\n- Drag \u0026 drop to reorder columns in the header.\n- Column resize via drag with per‑column min width.\n- Filters: text, number (int/double, ranges), boolean, date, enum (single/multi; IN/NOT IN/EQUALS) with built‑in\n  `FilterPanel`.\n- Active filters header above the table (chips + “Clear all”).\n- Row selection modes: None / Single / Multiple; optional striped rows.\n- Embedded (nested) tables via the `embedded` flag and `rowEmbedded` slot for building master–detail layouts inside\n  a single table.\n- Extensive customization via `TableCustomization` (background/content color, elevation, borders, typography,\n  alignment). Outer table border is configurable via `border` parameter (custom stroke or disabled entirely).\n- i18n via `StringProvider` (default `DefaultStrings`).\n- Targets: Android / JVM (Desktop) / JS (Web) / iOS (KMP source sets present; targets enabled via project conventions).\n- Pinned columns with configurable side (left/right) and count.\n\n### Installation\n\nAdd repository (usually `mavenCentral`) and include the modules you need:\n\n```kotlin\ndependencies {\n    implementation(\"ua.wwind.table-kmp:table-core:1.7.14\")\n    // optional\n    implementation(\"ua.wwind.table-kmp:table-format:1.7.14\")\n    implementation(\"ua.wwind.table-kmp:table-paging:1.7.14\")\n}\n```\n\nThe project uses `kotlinx-collections-immutable` for all table/state collections to ensure predictable, thread-safe\nstate management and efficient Compose recomposition:\n\n```kotlin\ndependencies {\n    implementation(\"org.jetbrains.kotlinx:kotlinx-collections-immutable:\u003clatest-version\u003e\")\n}\n```\n\nOpt‑in to experimental API on call sites that use the table:\n\n```kotlin\n@OptIn(ExperimentalTableApi::class)\n@Composable\nfun MyScreen() { /* ... */\n}\n```\n\n### Compatibility\n\nThe following table lists compatibility information for released library versions.\n\n| Version | Kotlin | Compose Multiplatform |\n|---------|-------:|----------------------:|\n| 1.7.13  | 2.3.10 |                1.10.1 |\n| 1.7.4   |  2.3.0 |                 1.9.3 |\n| 1.4.0   | 2.2.21 |                 1.9.3 |\n| 1.3.1   | 2.2.21 |                 1.9.2 |\n| 1.2.1   | 2.2.10 |                 1.9.0 |\n\n### Quick start\n\n#### 1) Model and fields\n\n```kotlin\ndata class Person(val name: String, val age: Int)\n\nenum class PersonField { Name, Age }\n```\n\n#### 2) Columns (DSL `tableColumns`)\n\n```kotlin\nval columns = tableColumns\u003cPerson, PersonField, PersonTableData\u003e {\n    column(PersonField.Name, valueOf = { it.name }) {\n        header(\"Name\")\n        cell { person, _ -\u003e Text(person.name) }\n        sortable()\n        // Enable built‑in Text filter UI in header\n        filter(TableFilterType.TextTableFilter())\n        // Auto‑fit to content with optional max cap\n        autoWidth(max = 500.dp)\n\n        // Optional footer with access to table data\n        footer { tableData -\u003e\n            Text(\"Total: ${tableData.displayedPeople.size}\")\n        }\n    }\n\n    column(PersonField.Age, valueOf = { it.age }) {\n        header(\"Age\")\n        cell { person, _ -\u003e Text(person.age.toString()) }\n        sortable()\n        align(Alignment.End)\n        filter(\n            TableFilterType.NumberTableFilter(\n                delegate = TableFilterType.NumberTableFilter.IntDelegate,\n                rangeOptions = 0 to 120\n            )\n        )\n    }\n}\n```\n\nColumn options: `sortable`, `resizable`, `visible`, `width(min, pref)`, `autoWidth(max)`, `align(...)`,\n`rowHeight(min, max)`, `filter(...)`, `groupHeader(...)`, `headerDecorations(...)`, `headerClickToSort(...)`,\n`footer(...)`.\n\n#### 3) Table state\n\n```kotlin\nval state = rememberTableState(\n    columns = columns.map { it.key },\n    settings = TableSettings(\n        stripedRows = true,\n        showActiveFiltersHeader = true,\n        selectionMode = SelectionMode.Single,\n    )\n)\n```\n\nYou can also provide `initialOrder`, `initialWidths`, `initialSort` and update from outside using\n`state.setColumnOrder(...)`, `state.setColumnWidths(...)`.\n\n#### 4) Rendering (core)\n\n```kotlin\n@Composable\nfun PeopleTable(items: List\u003cPerson\u003e) {\n    Table(\n        itemsCount = items.size,\n        itemAt = { index -\u003e items.getOrNull(index) },\n        state = state,\n        columns = columns,\n        onRowClick = { person -\u003e /* ... */ },\n    )\n}\n```\n\nUseful parameters: `placeholderRow`, `contextMenu` (long‑press/right‑click),\n`colors = TableDefaults.colors()`, `icons = TableHeaderDefaults.icons()`,\n`border` (outer border stroke; `null` uses theme default, `TableDefaults.NoBorder` disables border).\n\n### Cell editing mode\n\nThe table supports row‑scoped cell editing with custom edit UI, validation and keyboard navigation.\n\n- **Table‑level switch**: enable editing via `TableSettings(editingEnabled = true)`.\n- **Editable table**: use `EditableTable\u003cT, C, E\u003e` when you need editing support.\n- **Table data parameter**: the generic parameter `E` represents table data (shared state) accessible in headers,\n  footers, and edit cells. This allows passing validation errors, aggregated values, or any other table-wide state.\n- **Editable columns DSL**: declare columns with `editableTableColumns\u003cT, C, E\u003e { ... }` and per‑cell `editCell`.\n- **Callbacks**: validate and react to edit lifecycle with `onRowEditStart`, `onRowEditComplete`, `onEditCancelled`.\n- **Keyboard**: Enter/Done moves to the next editable cell; Escape cancels editing (desktop targets).\n\n#### TableCellTextField: text field adapted for table editing\n\nFor text editing inside table cells there is a dedicated composable `TableCellTextField`:\n\n- **Focus integration**: it is already wired to the table focus system via `syncEditCellFocus()` on its `Modifier`.\n  This ensures that when a row enters edit mode, the correct cell receives focus, and that keyboard navigation\n  (Enter/Done to move to the next editable cell, Escape to cancel) works consistently across targets.\n- **Compact layout**: by default it uses reduced paddings and no border to better fit into dense table rows.\n- **Visual consistency**: styles and colors match Material 3 inputs used in the rest of the table UI.\n\nWhenever you build text‑based edit UI for a cell, prefer `TableCellTextField` over a raw `TextField`/\n`BasicTextField`. This way you get correct focus behavior and table‑aware UX without any additional setup.\n\nMinimal example with `TableCellTextField`:\n\n```kotlin\ndata class Person(val id: Int, val name: String, val age: Int)\n\n// Table data containing displayed items and edit state\ndata class PersonTableData(\n    val displayedPeople: List\u003cPerson\u003e = emptyList(),\n    val editState: PersonEditState = PersonEditState(),\n)\n\n// Per‑row edit state (validation, errors, etc.)\ndata class PersonEditState(\n    val person: Person? = null,\n    val nameError: String = \"\",\n    val ageError: String = \"\",\n)\n\nenum class PersonColumn { NAME, AGE }\n\nval settings = TableSettings(\n    editingEnabled = true,\n    rowHeightMode = RowHeightMode.Dynamic,\n)\n\nval state = rememberTableState(\n    columns = PersonColumn.entries.toImmutableList(),\n    settings = settings,\n)\n\n// Editable columns definition\nval columns = editableTableColumns\u003cPerson, PersonColumn, PersonTableData\u003e {\n    column(PersonColumn.NAME, valueOf = { it.name }) {\n        title { \"Name\" }\n        cell { person, _ -\u003e Text(person.name) }\n\n        // Edit UI for the cell; table decides when to show it\n        editCell { person, tableData, onComplete -\u003e\n            var text by remember(person) { mutableStateOf(person.name) }\n\n            TableCellTextField(\n                value = text,\n                onValueChange = { text = it },\n                isError = tableData.editState.nameError.isNotEmpty(),\n                keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),\n                keyboardActions = KeyboardActions(onDone = { onComplete() }),\n            )\n        }\n\n        // Footer with access to table data\n        footer { tableData -\u003e\n            Text(\"Total: ${tableData.displayedPeople.size}\")\n        }\n    }\n\n    column(PersonColumn.AGE, valueOf = { it.age }) {\n        title { \"Age\" }\n        cell { person, _ -\u003e Text(person.age.toString()) }\n\n        editCell { person, tableData, onComplete -\u003e\n            var text by remember(person) { mutableStateOf(person.age.toString()) }\n\n            TableCellTextField(\n                value = text,\n                onValueChange = { input -\u003e\n                    text = input.filter { it.isDigit() }\n                },\n                keyboardOptions = KeyboardOptions(\n                    keyboardType = KeyboardType.Number,\n                    imeAction = ImeAction.Done,\n                ),\n                keyboardActions = KeyboardActions(onDone = { onComplete() }),\n            )\n        }\n    }\n}\n\n// Somewhere in your screen\nEditableTable(\n    itemsCount = people.size,\n    itemAt = { index -\u003e people.getOrNull(index) },\n    state = state,\n    columns = columns,\n    tableData = currentTableData, // your PersonTableData instance\n    onRowEditStart = { person, rowIndex -\u003e\n        // Initialize edit state for the row\n    },\n    onRowEditComplete = { rowIndex -\u003e\n        // Validate and persist; return true to exit edit mode, false to keep editing\n        true\n    },\n    onEditCancelled = { rowIndex -\u003e\n        // Optional: revert in‑memory changes\n    },\n)\n```\n\n#### Focus handling for custom edit implementations\n\nIf you build custom edit content that includes its own text field implementation or composite inputs, you should\nintegrate with the table focus handling. There are two options:\n\n- **Use `TableCellTextField` directly**: this is the recommended and simplest way. It already calls\n  `syncEditCellFocus()` on its `modifier`, so the cell participates in the table focus chain automatically.\n- **Reuse the focus modifier in custom components**: if you must write your own text field wrapper, make sure to\n  apply the same modifier:\n\n```kotlin\n@Composable\nfun CustomCellEditor(\n    value: String,\n    onValueChange: (String) -\u003e Unit,\n) {\n    BasicTextField(\n        value = value,\n        onValueChange = onValueChange,\n        modifier = Modifier\n            .fillMaxWidth()\n            .syncEditCellFocus(),\n    )\n}\n```\n\nThe `syncEditCellFocus()` modifier performs the following table‑specific work:\n\n- **Tracks the active edit cell** and requests focus when its row/column become active.\n- **Releases focus and clears selection** when editing ends or moves to another cell.\n- **Coordinates keyboard navigation** so that `onComplete` in `editCell` moves to the next editable cell and\n  eventually triggers `onRowEditComplete`.\n\nBy either using `TableCellTextField` or reusing `syncEditCellFocus()` in your own composables, custom edit UIs stay\nconsistent with the default table editing behavior.\n\nRuntime behavior:\n- Double‑click on an editable cell to enter **row edit mode**.\n- All editable cells in the row render their `editCell` content.\n- Press **Enter/Done** in a cell to call `onComplete()` and move to the next editable column.\n- After the last editable cell, `onRowEditComplete` is invoked; returning `false` keeps the row in edit mode.\n- Press **Escape** to cancel editing and trigger `onEditCancelled` (desktop targets).\n\n### Data grouping\n\nGroup table data by any column to organize and visualize hierarchical relationships:\n\n```kotlin\n// Enable grouping programmatically\nstate.groupBy = PersonField.Department\n\n// Or let users group via header dropdown menu\n// (automatically available for all columns)\n```\n\nCustomize group header appearance and content:\n\n```kotlin\ncolumn(PersonField.Department, valueOf = { it.department }) {\n    header(\"Department\")\n    cell { person, _ -\u003e Text(person.department) }\n\n    // Custom group header renderer\n    groupHeader { groupValue -\u003e\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n            modifier = Modifier.padding(8.dp)\n        ) {\n            Icon(Icons.Default.Group, contentDescription = null)\n            Spacer(modifier = Modifier.width(8.dp))\n            Text(\n                text = \"Department: $groupValue\",\n                style = MaterialTheme.typography.titleMedium,\n                fontWeight = FontWeight.Bold\n            )\n        }\n    }\n}\n```\n\nGroup headers are sticky and remain visible during scrolling. Configure group content alignment via table settings:\n\n```kotlin\nval state = rememberTableState(\n    columns = columns.map { it.key },\n    settings = TableSettings(\n        groupContentAlignment = Alignment.CenterStart,\n        // ... other settings\n    )\n)\n```\n\n### Footer row\n\nDisplay a summary footer row at the bottom of the table with custom content per column. Footer receives table data as a\nparameter, allowing access to displayed items and other table state:\n\n```kotlin\ndata class PersonTableData(\n    val displayedPeople: List\u003cPerson\u003e,\n    val editState: PersonEditState,\n)\n\nval columns = tableColumns\u003cPerson, PersonField, PersonTableData\u003e {\n    column(PersonField.Name, valueOf = { it.name }) {\n        header(\"Name\")\n        cell { person, _ -\u003e Text(person.name) }\n\n        // Footer content with access to table data (Unit for non-editable tables)\n        footer { tableData -\u003e\n            Text(\n                text = \"Total: ${tableData.displayedPeople.size}\",\n                fontWeight = FontWeight.Bold\n            )\n        }\n    }\n}\n```\n\nConfigure footer behavior via table settings:\n\n```kotlin\nval state = rememberTableState(\n    columns = columns.map { it.key },\n    settings = TableSettings(\n        showFooter = true,      // Enable footer display\n        footerPinned = true,    // Pin footer at bottom (default)\n        // ... other settings\n    )\n)\n```\n\nFooter options:\n\n- **showFooter**: Enable or disable footer row display.\n- **footerPinned**: When `true` (default), footer stays visible at the bottom of the table viewport, similar to a sticky\n  header. When `false`, footer scrolls with table content.\n- **footerHeight**: Customize footer height via `TableDimensions.footerHeight`.\n- **footerColors**: Customize footer colors via `TableColors.footerContainerColor` and `TableColors.footerContentColor`.\n\nThe footer:\n\n- Respects column widths and alignment settings from the main table.\n- Supports pinned columns just like header and body rows.\n- Synchronizes horizontal scrolling with the rest of the table.\n- For embedded tables, footer is always non-pinned and scrolls with content.\n\n### Paging integration (`table-paging`)\n\n```kotlin\n@Composable\nfun PeoplePagingTable(paging: PagingData\u003cPerson\u003e) {\n    Table(\n        items = paging,\n        state = state,\n        columns = columns,\n    )\n}\n```\n\nThere is also `LazyListScope.handleLoadState(...)` to render loading/empty states.\n\n### Conditional formatting (`table-format`)\n\n- Build a `TableCustomization` from rules via `rememberCustomization(rules, matches = ...)`. Row‑wide rules have\n  `columns = emptyList()`; cell‑specific rules list field keys in `columns`.\n- Use `FormatDialog(...)` to create/edit rules (Design / Condition / Fields tabs).\n\n```kotlin\n// 1) Rules\nval rules = remember {\n    listOf(\n        TableFormatRule.new\u003cPersonField, Person\u003e(id = 1, filter = Person(\"\", 0))\n    )\n}\n\n// 2) Matching logic\nval customization = rememberCustomization\u003cPerson, PersonField, Person\u003e(\n    rules = rules,\n    matches = { item, filter -\u003e item.age \u003e= 65 },\n)\n\n// 3) Pass customization to the table\nTable(\n    itemsCount = items.size,\n    itemAt = { index -\u003e items.getOrNull(index) },\n    state = state,\n    columns = columns,\n    customization = customization,\n)\n\n// 4) Optional: rules editor dialog\nFormatDialog(\n    showDialog = show,\n    rules = rules,\n    onRulesChanged = { /* persist */ },\n    getNewRule = { id -\u003e TableFormatRule.new\u003cPersonField, Person\u003e(id, Person(\"\", 0)) },\n    getTitle = { field -\u003e field.name },\n    filters = { rule, onApply -\u003e /* return list of FormatFilterData for fields */ emptyList() },\n    entries = PersonField.entries,\n    key = Unit,\n    strings = DefaultStrings,\n    onDismissRequest = { /* ... */ },\n)\n```\n\n`rememberCustomization` merges base styles with matching rules into a resulting `TableCustomization` (background,\ncontent color, text style, alignment, etc.).\n\n### Core API reference (table-core)\n\n- **Composable `Table\u003cT, C\u003e`**: renders header and virtualized rows for read-only tables (tableData = Unit).\n    - **Required**: `itemsCount`, `itemAt(index)`, `state: TableState\u003cC\u003e`, `columns: List\u003cColumnSpec\u003cT, C, Unit\u003e\u003e`.\n    - **Slots**: `placeholderRow()`.\n    - **UX**: `onRowClick`, `onRowLongClick`, `contextMenu(item, pos, dismiss)`.\n    - **Look**: `customization`, `colors = TableDefaults.colors()`, `icons = TableHeaderDefaults.icons()`, `strings`,\n      `shape`, `border` (outer border; `null` = theme default, `TableDefaults.NoBorder` = no border).\n    - **Scroll**: optional `verticalState`, `horizontalState`.\n    - **Embedded content**: `embedded` flag and `rowEmbedded` slot let you render nested detail content or even a\n      secondary table inside each row, while still reusing the same table state, filters and formatting rules.\n- **Composable `Table\u003cT, C, E\u003e`**: overload that accepts custom table data for headers, footers, and edit cells.\n    - **Additional parameter**: `tableData: E` - shared state accessible in headers, footers, custom filters, and edit\n      cells.\n    - All other parameters same as read-only variant.\n- **Composable `EditableTable\u003cT, C, E\u003e`**: renders header and virtualized rows with editing support.\n    - **Additional parameters**: `tableData: E`, `onRowEditStart`, `onRowEditComplete`, `onEditCancelled`.\n    - Columns must use `ColumnSpec\u003cT, C, E\u003e` with `E` matching the tableData type.\n- **Columns DSL**:\n    - `tableColumns\u003cT, C, E\u003e { ... }` produces `List\u003cColumnSpec\u003cT, C, E\u003e\u003e` for read-only tables.\n    - `editableTableColumns\u003cT, C, E\u003e { ... }` produces `List\u003cColumnSpec\u003cT, C, E\u003e\u003e` for editable tables.\n    - Column configuration:\n        - Cell: `cell { item, tableData -\u003e ... }` for regular cell content with access to table data (use `_` if table\n          data is not needed).\n        - Header: `header(\"Text\")` or `header(tableData) { ... }`; optional `title { \"Name\" }` for active filter chips.\n        - Footer: `footer(tableData) { ... }` for custom footer cell content with access to table data.\n        - Editing: `editCell { item, tableData, onComplete -\u003e ... }` for custom edit UI.\n        - Sorting: `sortable()`, `headerClickToSort(Boolean)`.\n        - Filters UI: `filter(TableFilterType.*)`.\n        - Sizing: `width(min, pref)`, `autoWidth(max)`, `resizable(Boolean)`, `align(Alignment.Horizontal)`.\n        - Row height hints: `rowHeight(min, max)` used when `rowHeightMode = Dynamic`.\n          - Decorations: `headerDecorations(Boolean)` to hide built‑ins when fully customizing header.\n- **Header customization**\n    - When `headerDecorations = true` (default), the table places sort and filter icons automatically.\n    - For a fully custom header, set `headerDecorations(false)` and use helpers inside `header { ... }`:\n\n```kotlin\ncolumn(PersonField.Name, valueOf = { it.name }) {\n    headerDecorations(false)\n    header {\n        Row(verticalAlignment = Alignment.CenterVertically) {\n            Text(\"Name\", modifier = Modifier.padding(end = 8.dp))\n            TableHeaderSortIcon()\n            TableHeaderFilterIcon()\n        }\n    }\n    sortable()\n    filter(TableFilterType.TextTableFilter())\n}\n```\n\n- **State**: `rememberTableState(columns, initialSort?, initialOrder?, initialWidths?, settings?, dimensions?)`.\n    - Sorting: `state.setSort(column, order?)`; current `state.sort`.\n    - Grouping: `state.groupBy(column)` to enable grouping; `state.groupBy(null)` to disable.\n    - Column order/size: `state.setColumnOrder(order)`, `state.resizeColumn(column, Set/Reset)`,\n      `state.setColumnWidths(map)`.\n    - Auto-width recalculation: `state.recalculateAutoWidths()` to manually recompute column\n      widths based on current content measurements. Useful for deferred/paginated data loading where initial auto-width\n      calculation happened on empty data.\n    - Filters: `state.setFilter(column, TableFilterState(...))`; current per‑column `state.filters`.\n    - Selection: `state.toggleSelect(index)`, `state.toggleCheck(index)`, `state.toggleCheckAll(count)`,\n      `state.selectCell(row, column)`.\n- **Settings and geometry**\n    - `TableSettings`: `isDragEnabled`, `autoApplyFilters`, `autoFilterDebounce`, `stripedRows`,\n      `showActiveFiltersHeader`, `selectionMode: None/Single/Multiple`, `groupContentAlignment`,\n      `rowHeightMode: Fixed/Dynamic`, `enableDragToScroll` (controls whether drag-to-scroll is enabled; when disabled,\n      traditional scrollbars are used instead), `editingEnabled` (master switch for cell editing mode), `showFooter`\n      (enable footer row display), `footerPinned` (pin footer at bottom or scroll with content),\n      `enableTextSelection` (wrap table body in `SelectionContainer` to allow text selection; defaults to `false`),\n      `showVerticalDividers` (show/hide vertical dividers between columns; defaults to `true`),\n      `showRowDividers` (show/hide horizontal dividers between rows; defaults to `true`),\n      `showHeaderDivider` (show/hide horizontal divider below header; defaults to `true`),\n      `showFastFiltersDivider` (show/hide horizontal divider below fast filters row; defaults to `true`).\n    - `TableDimensions`: `defaultColumnWidth`, `defaultRowHeight`, `footerHeight`, `checkBoxColumnWidth`,\n      `verticalDividerThickness`, `verticalDividerPaddingHorizontal`.\n    - `TableColors`: via `TableDefaults.colors(...)`.\n\n### Filters (built‑in types)\n\n- **TextTableFilter**: contains/starts/ends/equals/is_null/is_not_null.\n- **NumberTableFilter(Int/Double)**: gt/gte/lt/lte/equals/not_equals/between/is_null/is_not_null + optional range slider\n  via `rangeOptions`.\n- **BooleanTableFilter**: equals; optional `getTitle(BooleanType)`.\n- **DateTableFilter**: gt/gte/lt/lte/equals/between/is_null/is_not_null (uses `kotlinx.datetime.LocalDate`).\n- **EnumTableFilter\u003cT: Enum\u003cT\u003e\u003e**: in/not_in/equals with `options: List\u003cT\u003e` and `getTitle(T)`.\n- **CustomTableFilter\u003cT, E\u003e**: fully custom filter UI and state with access to table data. Implement\n  `CustomFilterRenderer\u003cT, E\u003e` for main panel and\n  optional fast filter (both receive `tableData: E` parameter), and `CustomFilterStateProvider\u003cT\u003e` for chip text.\n  Supports data visualizations of any complexity, including dynamic histograms and statistics based on current table\n  data.\n- **DisabledTableFilter**: special marker filter type that completely disables filtering for a column while keeping\n  the API contract (no filter UI is rendered for such columns in filter panels and conditional formatting dialogs).\n\nApplying filters to data is app‑specific. Example:\n\n```kotlin\nval filtered = remember(items, state.filters) {\n    items.filter { item -\u003e\n        // Evaluate your domain against active state.filters\n        // See `table-sample` for a full example\n        true\n    }\n}\n```\n\n### Fast Filters\n\nFast filters provide quick inline filtering directly in a dedicated row below the header. They share the same\n`TableFilterState` as main filters but with simplified UI and pre-set default constraints:\n\n- **Location**: Rendered as a horizontal row below the header when `settings.showFastFilters = true` and at least\n  one visible column has a filter configured (not `null` or `DisabledTableFilter`).\n- **Synchronized state**: Fast filters and main filter panels use the same `state.filters`, changes in one immediately\n  reflect in the other.\n- **Default constraints**: Each fast filter type uses a sensible default:\n    - `TextTableFilter` → CONTAINS\n    - `NumberTableFilter` → EQUALS\n    - `BooleanTableFilter` → EQUALS (tri-state checkbox)\n    - `DateTableFilter` → EQUALS (date picker)\n    - `EnumTableFilter` → EQUALS (dropdown)\n    - `CustomTableFilter` → fully custom (implement `RenderFastFilter` or leave empty)\n- **Auto-apply**: Fast filters always apply changes automatically with debounce (controlled by\n  `settings.autoFilterDebounce`).\n\nFast filters are ideal for quick data exploration and filtering without opening the full filter panel dialog.\n\n### Selection\n\n- `SelectionMode.None` (default), `Single`, `Multiple`.\n- In Multiple mode, you can handle selection programmatically:\n\n```kotlin\nTable(\n    itemsCount = items.size,\n    itemAt = { index -\u003e items[index] },\n    state = state,\n    columns = columns,\n    onRowClick = { _ -\u003e state.toggleCheck(/* row index comes from key or context */) }\n)\n```\n\n### Checkbox selection with tableData\n\nThe `tableData` parameter enables implementing custom checkbox-based selection that shares state between cells, headers,\nand external UI components. This pattern is useful when you need:\n\n- Custom selection logic independent of the built-in `SelectionMode`\n- A dedicated checkbox column with select-all functionality in the header\n- External UI (e.g., floating action bar) that reacts to selection state\n- Bulk operations on selected items (delete, export, etc.)\n\n#### 1) Define table data with selection state\n\n```kotlin\ndata class Person(val id: Int, val name: String, val age: Int)\n\nenum class PersonColumn { SELECTION, NAME, AGE }\n\n// Table data containing selection state\ndata class PersonTableData(\n    val displayedPeople: List\u003cPerson\u003e = emptyList(),\n    val selectedIds: Set\u003cInt\u003e = emptySet(),\n    val selectionModeEnabled: Boolean = false,\n)\n```\n\n#### 2) Create a checkbox column using tableData\n\n```kotlin\nval columns = tableColumns\u003cPerson, PersonColumn, PersonTableData\u003e {\n    // Checkbox column for selection\n    column(PersonColumn.SELECTION, valueOf = { it.id }) {\n        width(48.dp, 48.dp)\n        resizable(false)\n\n        // Cell renders checkbox based on selection state from tableData\n        cell { person, tableData -\u003e\n            if (tableData.selectionModeEnabled) {\n                Box(\n                    contentAlignment = Alignment.Center,\n                    modifier = Modifier.fillMaxSize(),\n                ) {\n                    Checkbox(\n                        checked = person.id in tableData.selectedIds,\n                        onCheckedChange = { onToggleSelection(person.id) },\n                    )\n                }\n            }\n        }\n\n        // Header renders tri-state checkbox for select all/none\n        header { tableData -\u003e\n            if (tableData.selectionModeEnabled) {\n                val displayedIds = tableData.displayedPeople.map { it.id }.toSet()\n                val selectedCount = displayedIds.count { it in tableData.selectedIds }\n                val toggleState = when (selectedCount) {\n                    0 -\u003e ToggleableState.Off\n                    displayedIds.size -\u003e ToggleableState.On\n                    else -\u003e ToggleableState.Indeterminate\n                }\n                Box(\n                    contentAlignment = Alignment.Center,\n                    modifier = Modifier.fillMaxSize(),\n                ) {\n                    TriStateCheckbox(\n                        state = toggleState,\n                        onClick = { onToggleSelectAll() },\n                    )\n                }\n            }\n        }\n    }\n\n    // Other columns...\n    column(PersonColumn.NAME, valueOf = { it.name }) {\n        title { \"Name\" }\n        cell { person, _ -\u003e Text(person.name) }\n    }\n}\n```\n\n#### 3) Manage selection state in ViewModel\n\n```kotlin\nclass MyViewModel : ViewModel() {\n    private val _people = MutableStateFlow\u003cList\u003cPerson\u003e\u003e(loadPeople())\n    private val _selectedIds = MutableStateFlow\u003cSet\u003cInt\u003e\u003e(emptySet())\n    private val _selectionModeEnabled = MutableStateFlow(false)\n\n    val tableData: StateFlow\u003cPersonTableData\u003e = combine(\n        _people,\n        _selectedIds,\n        _selectionModeEnabled,\n    ) { people, selected, enabled -\u003e\n        PersonTableData(\n            displayedPeople = people,\n            selectedIds = selected,\n            selectionModeEnabled = enabled,\n        )\n    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), PersonTableData())\n\n    fun setSelectionMode(enabled: Boolean) {\n        _selectionModeEnabled.value = enabled\n        if (!enabled) _selectedIds.value = emptySet()\n    }\n\n    fun toggleSelection(personId: Int) {\n        _selectedIds.update { current -\u003e\n            if (personId in current) current - personId else current + personId\n        }\n    }\n\n    fun toggleSelectAll() {\n        val displayedIds = _people.value.map { it.id }.toSet()\n        _selectedIds.update { current -\u003e\n            if (displayedIds.all { it in current }) {\n                current - displayedIds  // Deselect all\n            } else {\n                current + displayedIds  // Select all\n            }\n        }\n    }\n\n    fun deleteSelected() {\n        val idsToDelete = _selectedIds.value\n        _people.update { it.filter { person -\u003e person.id !in idsToDelete } }\n        _selectedIds.value = emptySet()\n    }\n}\n```\n\n#### 4) Render table with external action bar\n\n```kotlin\n@Composable\nfun PeopleScreen(viewModel: MyViewModel) {\n    val tableData by viewModel.tableData.collectAsState()\n\n    Box(modifier = Modifier.fillMaxSize()) {\n        Table(\n            itemsCount = tableData.displayedPeople.size,\n            itemAt = { tableData.displayedPeople.getOrNull(it) },\n            state = state,\n            columns = columns,\n            tableData = tableData,\n        )\n\n        // Floating action bar shown when items are selected\n        if (tableData.selectedIds.isNotEmpty()) {\n            Surface(\n                modifier = Modifier.align(Alignment.BottomCenter).padding(16.dp),\n                shape = RoundedCornerShape(24.dp),\n                color = MaterialTheme.colorScheme.primaryContainer,\n            ) {\n                Row(\n                    modifier = Modifier.padding(16.dp),\n                    horizontalArrangement = Arrangement.spacedBy(16.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Text(\"${tableData.selectedIds.size} selected\")\n                    Button(onClick = { viewModel.deleteSelected() }) {\n                        Text(\"Delete\")\n                    }\n                }\n            }\n        }\n    }\n}\n```\n\n#### Key benefits of this pattern\n\n- **Reactive UI**: Checkbox state updates instantly when `tableData.selectedIds` changes.\n- **Centralized state**: Selection logic lives in ViewModel, making it testable and reusable.\n- **Flexible visibility**: Show/hide the checkbox column by controlling width via `state.setColumnWidths()`.\n- **Bulk operations**: Easy to implement delete, export, or other actions on selected items.\n- **Header integration**: Tri-state checkbox in header provides intuitive select-all UX.\n\n### Dynamic row height and auto‑width\n\n- Dynamic height: set `rowHeightMode = RowHeightMode.Dynamic`. Use per‑column `rowHeight(min, max)` to hint bounds.\n- Auto‑width: call `autoWidth(max?)` in column builder. The table measures header + first batch of rows and applies\n  widths once per phase. Double‑click the header resizer to snap a column to its measured max content width.\n- Alternatively, use `state.recalculateAutoWidths()` to manually trigger width recalculation based on\n  current content measurements (useful for deferred/paginated data loading scenarios).\n\n### Drag-to-scroll\n\nBy default, the table enables drag-to-scroll functionality, allowing users to pan the table content by dragging with\nmouse or touch gestures. While this works well on mobile devices, it may not be ideal for desktop environments where\ntraditional scrollbars and mouse wheel navigation are preferred.\n\nTo disable drag-to-scroll and use standard scrollbars instead:\n\n```kotlin\nval state = rememberTableState(\n    columns = columns.map { it.key },\n    settings = TableSettings(\n        enableDragToScroll = false, // Disable drag-to-scroll\n        // ... other settings\n    )\n)\n```\n\nWhen `enableDragToScroll = false`:\n\n- Mouse dragging will not scroll the table\n- Horizontal and vertical scrollbars will be available\n- Mouse wheel and trackpad gestures will work normally\n- Better compatibility with cell selection and text selection workflows\n\n### Custom header icons\n\nCustomize sort/filter icons:\n\n```kotlin\nval icons = TableHeaderDefaults.icons(\n    sortAsc = MyUp,\n    sortDesc = MyDown,\n    sortNeutral = MySort,\n    filterActive = MyFilterFilled,\n    filterInactive = MyFilterOutline\n)\n\nTable(\n    itemsCount = items.size,\n    itemAt = { index -\u003e items[index] },\n    state = state,\n    columns = columns,\n    icons = icons\n)\n```\n\n### Conditional formatting (table-format)\n\n- Build `TableCustomization` from rules using `rememberCustomization(rules, matches = ...)`. Row‑wide rules have\n  `columns = emptyList()`; cell‑specific rules list field keys in `columns`.\n- Use `FormatDialog(...)` to let users create/edit rules.\n\nMinimal example:\n\n```kotlin\ndata class Person(val name: String, val age: Int, val rating: Int)\nenum class PersonField { Name, Age, Rating }\n\n// Rules\nval rules = remember {\n    val ratingFilter: Map\u003cPersonField, TableFilterState\u003c*\u003e\u003e =\n        mapOf(\n            PersonField.Rating to TableFilterState(\n                constraint = FilterConstraint.GTE,\n                values = listOf(4),\n            ),\n        )\n    val ratingRule =\n        TableFormatRule\u003cPersonField, Map\u003cPersonField, TableFilterState\u003c*\u003e\u003e\u003e(\n            id = 1L,\n            enabled = true,\n            base = false,\n            columns = listOf(PersonField.Rating),\n            cellStyle = TableCellStyleConfig(\n                contentColor = 0xFFFFD700.toInt(), // Gold\n            ),\n            filter = ratingFilter,\n        )\n    listOf(ratingRule)\n}\n\n// Matching logic (app‑specific)\nval customization = rememberCustomization\u003cPerson, PersonField, Person\u003e(\n    rules = rules,\n    matches = { person, ruleFilters -\u003e\n        for ((column, stateAny) in ruleFilters) {\n            when (column) {\n                PersonField.Rating -\u003e {\n                    val value = person.rating\n                    val st = stateAny as TableFilterState\u003cInt\u003e\n                    val constraint = st.constraint ?: continue\n                    when (constraint) {\n                        FilterConstraint.GT -\u003e value \u003e (st.values?.getOrNull(0) ?: value)\n                        FilterConstraint.GTE -\u003e value \u003e= (st.values?.getOrNull(0) ?: value)\n                        FilterConstraint.LT -\u003e value \u003c (st.values?.getOrNull(0) ?: value)\n                        FilterConstraint.LTE -\u003e value \u003c= (st.values?.getOrNull(0) ?: value)\n                        FilterConstraint.EQUALS -\u003e value == (st.values?.getOrNull(0) ?: value)\n                        FilterConstraint.NOT_EQUALS -\u003e value != (st.values?.getOrNull(0) ?: value)\n                        FilterConstraint.BETWEEN -\u003e {\n                            val from = st.values?.getOrNull(0) ?: value\n                            val to = st.values?.getOrNull(1) ?: value\n                            from \u003c= value \u0026\u0026 value \u003c= to\n                        }\n\n                        else -\u003e true\n                    }\n                }\n                else -\u003e true\n            }\n        }\n    }\n)\n\nTable(\n    itemsCount = items.size,\n    itemAt = { index -\u003e items[index] },\n    state = state,\n    columns = columns,\n    customization = customization\n)\n\n// Optional dialog\nFormatDialog(\n    showDialog = show,\n    rules = rules,\n    onRulesChanged = { /* persist */ },\n    getNewRule = { id -\u003e TableFormatRule.new\u003cPersonField, Person\u003e(id, Person(\"\", 0)) },\n    getTitle = { it.name },\n    filters = { rule, onApply -\u003e emptyList() }, // build `FormatFilterData` list for your fields\n    entries = PersonField.values().toList(),\n    key = Unit,\n    strings = DefaultStrings,\n    onDismissRequest = { show = false }\n)\n```\n\nPublic API highlights:\n\n- `rememberCustomization\u003cT, C, FILTER\u003e(rules, matches = ...) : TableCustomization\u003cT, C\u003e`.\n- `TableFormatRule\u003cFIELD, FILTER\u003e` with `columns: List\u003cFIELD\u003e`, `cellStyle: TableCellStyleConfig`, `filter: FILTER`.\n- `FormatDialog(...)` and `FormatDialogSettings` for UX tweaks.\n- `FormatFilterData\u003cE\u003e` to describe per‑field filter controls in the dialog.\n- `FilterConstraint.isNullCheck()` extension function to check for IS_NULL/IS_NOT_NULL constraints.\n- `TableFilterState.isActive()` extension function to determine if a filter is active.\n- `VerticalScrollbarRenderer` and `VerticalScrollbarState` for custom scrollbar rendering in formatting dialogs.\n\n### Supported targets\n\n- Android, JVM (Desktop), JS (Web), iOS (KMP source sets present; targets enabled via project conventions).\n\n### Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=White-Wind-LLC/table\u0026type=date\u0026theme=dark\u0026legend=top-left)](https://www.star-history.com/#White-Wind-LLC/table\u0026type=date\u0026legend=top-left)\n\n### Third-Party Libraries\n\nThis project uses the following open source libraries:\n\n| Library                                                                                  | License            | Description                                                    |\n|------------------------------------------------------------------------------------------|--------------------|----------------------------------------------------------------|\n| [Reorderable](https://github.com/Calvin-LL/Reorderable)                                  | Apache License 2.0 | Drag and drop functionality for reordering items in Compose    |\n| [Paging for KMP](https://github.com/White-Wind-LLC/paging-kmp)                           | Apache License 2.0 | Kotlin Multiplatform paging library                            |\n| [ColorPicker Compose](https://github.com/skydoves/colorpicker-compose)                   | Apache License 2.0 | Color picker component for Jetpack Compose                     |\n| [Kermit](https://github.com/touchlab/Kermit)                                             | Apache License 2.0 | Kotlin Multiplatform logging library                           |\n\nAll third-party libraries are used in compliance with their respective licenses. For detailed license information, see\nthe individual library repositories linked above.\n\n### License\n\nLicensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwhite-wind-llc%2Ftable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwhite-wind-llc%2Ftable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwhite-wind-llc%2Ftable/lists"}