{"_id":"@anjunar/scalajs-ui-controls","_rev":"6-cf9ae245508e42eee0109ed290c7515f","name":"@anjunar/scalajs-ui-controls","dist-tags":{"latest":"1.1.0"},"versions":{"1.0.0":{"name":"@anjunar/scalajs-ui-controls","version":"1.0.0","author":{"name":"Patrick Bittner"},"license":"MIT","_id":"@anjunar/scalajs-ui-controls@1.0.0","maintainers":[{"name":"anjunar","email":"anjunar@gmx.de"}],"homepage":"https://github.com/anjunar/scalajs-ui#readme","bugs":{"url":"https://github.com/anjunar/scalajs-ui/issues"},"dist":{"shasum":"2b7804453539e2d70f2d2bd9a806c7d4c8ef9037","tarball":"https://registry.npmjs.org/@anjunar/scalajs-ui-controls/-/scalajs-ui-controls-1.0.0.tgz","fileCount":30,"integrity":"sha512-/7U20waAjPcl80n87gSTB1XvEgi578ti2BZU+rv4CNnZt7HdKQjGjg4J/aWYI2gNmXOWt4kBjWDRCNwCYD+DiA==","signatures":[{"sig":"MEYCIQC7+fi4vxuzqg1B6EyI3rJsFt7a4gvO4E7vA/P0oTQOcQIhANCX43YxdwtHjiehOYztge/YdfByxJDo/2DFD4AxCjlK","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":102571},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"gitHead":"a9312d1aa728764c037b016ba66b703e003d6d50","scripts":{"test":"vitest run","build":"tsc -p tsconfig.json","verify":"npm run typecheck && npm run test && npm run test:consumer","prepack":"npm run build","typecheck":"npm run build && tsc -p tsconfig.test.json","test:watch":"vitest","test:consumer":"vitest run --config vitest.consumer.config.ts"},"_npmUser":{"name":"anjunar","email":"anjunar@gmx.de"},"repository":{"url":"git+https://github.com/anjunar/scalajs-ui.git","type":"git","directory":"npm/scalajs-ui-controls"},"_npmVersion":"11.17.0","description":"The TypeScript controls API of UI 3: tabs, carousel, and the virtualized table / data grid / list, projected onto the ui.control components that run in the Scala.js runtime published as @anjunar/scalajs-ui-bridge.","directories":{},"sideEffects":false,"_nodeVersion":"26.4.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^30.0.1","vitest":"^3.2.7","typescript":"^5.6.0","@types/node":"^22.0.0","@anjunar/scalajs-ui":"*","@anjunar/scalajs-ui-core":"*","@anjunar/scalajs-ui-bridge":"*"},"peerDependencies":{"@anjunar/scalajs-ui":"^1.0.0","@anjunar/scalajs-ui-core":"^1.0.0","@anjunar/scalajs-ui-bridge":"^1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/scalajs-ui-controls_1.0.0_1789143245346_0.6021293282741362","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"name":"@anjunar/scalajs-ui-controls","version":"1.0.2","author":{"name":"Patrick Bittner"},"license":"MIT","_id":"@anjunar/scalajs-ui-controls@1.0.2","maintainers":[{"name":"anjunar","email":"anjunar@gmx.de"}],"homepage":"https://github.com/anjunar/scalajs-ui#readme","bugs":{"url":"https://github.com/anjunar/scalajs-ui/issues"},"dist":{"shasum":"10fa23234e66b2cd81fba2486f0a4a992ab789a3","tarball":"https://registry.npmjs.org/@anjunar/scalajs-ui-controls/-/scalajs-ui-controls-1.0.2.tgz","fileCount":30,"integrity":"sha512-xaYzZcUJBAwoieap5rb2UD5pA17hXmzBddXoXQ/X8BTgIknVwCwkpDDWqSYX8iVMFdJKZLrqdsRVthwkPiyx5g==","signatures":[{"sig":"MEQCIBafosFoT5+u3i6/bmUR9+uWklaepm8TB0xdWQgRJ3tqAiB/ookRo6OWGQXXkQOtXmmpZzEcmlPc+RsBWpiyPYahNQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":145196},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"gitHead":"192dd9f15ce1652b461173756defb0046fbd9ac4","scripts":{"test":"vitest run","build":"tsc -p tsconfig.json","verify":"npm run typecheck && npm run test && npm run test:consumer","prepack":"npm run build","typecheck":"npm run build && tsc -p tsconfig.test.json","test:watch":"vitest","test:consumer":"vitest run --config vitest.consumer.config.ts"},"_npmUser":{"name":"anjunar","email":"anjunar@gmx.de"},"repository":{"url":"git+https://github.com/anjunar/scalajs-ui.git","type":"git","directory":"npm/scalajs-ui-controls"},"_npmVersion":"11.17.0","description":"The TypeScript controls API of Scala JS UI 1.0: tabs, carousel, and the virtualized table / data grid / list, projected onto the ui.control components that run in the Scala.js runtime published as @anjunar/scalajs-ui-bridge.","directories":{},"sideEffects":false,"_nodeVersion":"26.4.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^30.0.1","vitest":"^3.2.7","typescript":"^5.6.0","@types/node":"^22.0.0","@anjunar/scalajs-ui":"*","@anjunar/scalajs-ui-core":"*","@anjunar/scalajs-ui-bridge":"*"},"peerDependencies":{"@anjunar/scalajs-ui":"^1.0.2","@anjunar/scalajs-ui-core":"^1.0.2","@anjunar/scalajs-ui-bridge":"^1.0.2"},"_npmOperationalInternal":{"tmp":"tmp/scalajs-ui-controls_1.0.2_1789506026369_0.9389727276734854","host":"s3://npm-registry-packages-npm-production"}},"1.0.3":{"name":"@anjunar/scalajs-ui-controls","version":"1.0.3","author":{"name":"Patrick Bittner"},"license":"MIT","_id":"@anjunar/scalajs-ui-controls@1.0.3","maintainers":[{"name":"anjunar","email":"anjunar@gmx.de"}],"homepage":"https://github.com/anjunar/scalajs-ui#readme","bugs":{"url":"https://github.com/anjunar/scalajs-ui/issues"},"dist":{"shasum":"3f06713d23c0c2d6a7a27bda989b85c5e4e7a4dd","tarball":"https://registry.npmjs.org/@anjunar/scalajs-ui-controls/-/scalajs-ui-controls-1.0.3.tgz","fileCount":30,"integrity":"sha512-VZTAlsQ12uT2ONQvkyre1DeokI9XTdpPTrj302ZZNnm36BlJBoUE4IkGpkXaO+Y96ijt7Fj9VFpt5LE68+cZFQ==","signatures":[{"sig":"MEUCIDWM+HTN/57RG61XmwpnKCQ8LjyTeslM7SqzzKX258daAiEA1dJ3qVxAUlswB7NLKsg2auwQl2kAXxT4H7hlCzNY3Nk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":145196},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"gitHead":"81e89fc0d354046d9c9d548e5e1932d7baba0505","scripts":{"test":"vitest run","build":"tsc -p tsconfig.json","verify":"npm run typecheck && npm run test && npm run test:consumer","prepack":"npm run build","typecheck":"npm run build && tsc -p tsconfig.test.json","test:watch":"vitest","test:consumer":"vitest run --config vitest.consumer.config.ts"},"_npmUser":{"name":"anjunar","email":"anjunar@gmx.de"},"repository":{"url":"git+https://github.com/anjunar/scalajs-ui.git","type":"git","directory":"npm/scalajs-ui-controls"},"_npmVersion":"11.17.0","description":"The TypeScript controls API of Scala JS UI 1.0: tabs, carousel, and the virtualized table / data grid / list, projected onto the ui.control components that run in the Scala.js runtime published as @anjunar/scalajs-ui-bridge.","directories":{},"sideEffects":false,"_nodeVersion":"26.4.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^30.0.1","vitest":"^3.2.7","typescript":"^5.6.0","@types/node":"^22.0.0","@anjunar/scalajs-ui":"*","@anjunar/scalajs-ui-core":"*","@anjunar/scalajs-ui-bridge":"*"},"peerDependencies":{"@anjunar/scalajs-ui":"^1.0.3","@anjunar/scalajs-ui-core":"^1.0.3","@anjunar/scalajs-ui-bridge":"^1.0.3"},"_npmOperationalInternal":{"tmp":"tmp/scalajs-ui-controls_1.0.3_1789552899904_0.2558078433002542","host":"s3://npm-registry-packages-npm-production"}},"1.0.5":{"name":"@anjunar/scalajs-ui-controls","version":"1.0.5","author":{"name":"Patrick Bittner"},"license":"MIT","_id":"@anjunar/scalajs-ui-controls@1.0.5","maintainers":[{"name":"anjunar","email":"anjunar@gmx.de"}],"homepage":"https://github.com/anjunar/scalajs-ui#readme","bugs":{"url":"https://github.com/anjunar/scalajs-ui/issues"},"dist":{"shasum":"0d189e2bb1fa3851490a9c7771576bd3b522ecea","tarball":"https://registry.npmjs.org/@anjunar/scalajs-ui-controls/-/scalajs-ui-controls-1.0.5.tgz","fileCount":30,"integrity":"sha512-IeeZs8mJreJuRz6ygCjXck9GA2iNpyODh83EZxuqnEnEfTsVFu+MzCvQCjENBclqCyfcaLTy3PRO8WYstAuVXQ==","signatures":[{"sig":"MEQCIGGMYOMtD/mtCJutSQyG6kiLKEZZp4VWD3lIU7hMITN6AiA4rLqdVykI2/YBEAeRi2vx8rnjbPbmSU9dpDuXf4s9iw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEYCIQDlKRXZIqrn+u2BYzoHH4K+UvZOMeDnWjgwzGkBn5e/dAIhANpileEYrwwLXWg4R97MdVcqOaDGbpooiyTloWqDdDkL","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":168674},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"gitHead":"13988ebfcf64a17c3b7777ef28c3e55e6d48ddca","scripts":{"test":"vitest run","build":"tsc -p tsconfig.json","verify":"npm run typecheck && npm run test && npm run test:consumer","prepack":"npm run build","typecheck":"npm run build && tsc -p tsconfig.test.json","test:watch":"vitest","test:consumer":"vitest run --config vitest.consumer.config.ts"},"_npmUser":{"name":"anjunar","email":"anjunar@gmx.de"},"repository":{"url":"git+https://github.com/anjunar/scalajs-ui.git","type":"git","directory":"npm/scalajs-ui-controls"},"_npmVersion":"11.17.0","description":"The TypeScript controls API of Scala JS UI 1.0: tabs, carousel, and the virtualized table / data grid / list, projected onto the ui.control components that run in the Scala.js runtime published as @anjunar/scalajs-ui-bridge.","directories":{},"sideEffects":false,"_nodeVersion":"26.4.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^30.0.1","vitest":"^3.2.7","typescript":"^5.6.0","@types/node":"^22.0.0","@anjunar/scalajs-ui":"*","@anjunar/scalajs-ui-core":"*","@anjunar/scalajs-ui-bridge":"*"},"peerDependencies":{"@anjunar/scalajs-ui":"^1.0.5","@anjunar/scalajs-ui-core":"^1.0.5","@anjunar/scalajs-ui-bridge":"^1.0.5"},"_npmOperationalInternal":{"tmp":"tmp/scalajs-ui-controls_1.0.5_1789736140582_0.6294002589459375","host":"s3://npm-registry-packages-npm-production"}},"1.0.6":{"name":"@anjunar/scalajs-ui-controls","version":"1.0.6","author":{"name":"Patrick Bittner"},"license":"MIT","_id":"@anjunar/scalajs-ui-controls@1.0.6","maintainers":[{"name":"anjunar","email":"anjunar@gmx.de"}],"homepage":"https://github.com/anjunar/scalajs-ui#readme","bugs":{"url":"https://github.com/anjunar/scalajs-ui/issues"},"dist":{"shasum":"c792981f98d9ba7404a92e71fd8281f2df4670cd","tarball":"https://registry.npmjs.org/@anjunar/scalajs-ui-controls/-/scalajs-ui-controls-1.0.6.tgz","fileCount":30,"integrity":"sha512-2SpzfjRhEZgp1Rwm9jPmlX00QRXDjH6LllOJv0AZctKpD+bv3YDjkjm1n9g6xB2kAmZhkhBuipmucuuKezpm5w==","signatures":[{"sig":"MEUCIFLiOPEHss7Oqs5jFyehQRyiiKLdQNUTkB5bbUu/soQiAiEA0/dfM3h2vsRqx0tVHmfgLj1OdjhT0QqHb68voqa9vUU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIBvSugFCCXcEb1Uap4DW6MsYL5SNYY2IrJGLnxSiJmcYAiEA59KBsd1nzNNfDiOkxs0Nlab0l0gLdb/oQLt0WzdIj58=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":168674},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"gitHead":"b5f4e2ac72fff6d18aff21420f18a811c54578e0","scripts":{"test":"vitest run","build":"tsc -p tsconfig.json","verify":"npm run typecheck && npm run test && npm run test:consumer","prepack":"npm run build","typecheck":"npm run build && tsc -p tsconfig.test.json","test:watch":"vitest","test:consumer":"vitest run --config vitest.consumer.config.ts"},"_npmUser":{"name":"anjunar","email":"anjunar@gmx.de"},"repository":{"url":"git+https://github.com/anjunar/scalajs-ui.git","type":"git","directory":"npm/scalajs-ui-controls"},"_npmVersion":"11.17.0","description":"The TypeScript controls API of Scala JS UI 1.0: tabs, carousel, and the virtualized table / data grid / list, projected onto the ui.control components that run in the Scala.js runtime published as @anjunar/scalajs-ui-bridge.","directories":{},"sideEffects":false,"_nodeVersion":"26.4.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^30.0.1","vitest":"^3.2.7","typescript":"^5.6.0","@types/node":"^22.0.0","@anjunar/scalajs-ui":"*","@anjunar/scalajs-ui-core":"*","@anjunar/scalajs-ui-bridge":"*"},"peerDependencies":{"@anjunar/scalajs-ui":"^1.0.6","@anjunar/scalajs-ui-core":"^1.0.6","@anjunar/scalajs-ui-bridge":"^1.0.6"},"_npmOperationalInternal":{"tmp":"tmp/scalajs-ui-controls_1.0.6_1789958592407_0.5674899586382083","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"_id":"@anjunar/scalajs-ui-controls@1.1.0","bugs":{"url":"https://github.com/anjunar/scalajs-ui/issues"},"dist":{"shasum":"35926f93ecfb0b86b89bc4506ad299ab7f1cc931","tarball":"https://registry.npmjs.org/@anjunar/scalajs-ui-controls/-/scalajs-ui-controls-1.1.0.tgz","fileCount":30,"integrity":"sha512-TAOdAFy3ZdLd9kpMLXVBxWcHGoYOv3T35ara32ELMlT7AexUFfIbrckYpEvc2ApPFD7qL94ESCmtJMb51a8KJA==","signatures":[{"sig":"MEQCIGs6iF/erAOsBCWgFB4HmOndZqvomc35Y3toXzGGrr/mAiAOKXDvzUX2lN6hn0XHyc5QGewlOspMCHzZJDnO9PQKsQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQD/Aa8x/oJs6rG1FVPsRmAdJ7j3396sggblj3v/FQSkqgIgXHHL/PQOilAqpRSDzamqbui+iIkFPZPXYRSRRcl4uIY="}],"unpackedSize":169347},"main":"./dist/index.js","name":"@anjunar/scalajs-ui-controls","type":"module","types":"./dist/index.d.ts","author":{"name":"Patrick Bittner"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"gitHead":"a054cd17cb7cb398f4be3461b25c2a5df009a442","license":"MIT","scripts":{"test":"vitest run","build":"tsc -p tsconfig.json","verify":"npm run typecheck && npm run test && npm run test:consumer","prepack":"npm run build","typecheck":"npm run build && tsc -p tsconfig.test.json","test:watch":"vitest","test:consumer":"vitest run --config vitest.consumer.config.ts"},"version":"1.1.0","_npmUser":{"name":"anjunar","email":"anjunar@gmx.de"},"homepage":"https://github.com/anjunar/scalajs-ui#readme","repository":{"url":"git+https://github.com/anjunar/scalajs-ui.git","type":"git","directory":"npm/scalajs-ui-controls"},"_npmVersion":"11.17.0","description":"The TypeScript controls API of Scala JS UI 1.0: tabs, carousel, and the virtualized table / data grid / list, projected onto the ui.control components that run in the Scala.js runtime published as @anjunar/scalajs-ui-bridge.","directories":{},"maintainers":[{"name":"anjunar","email":"anjunar@gmx.de"}],"sideEffects":false,"_nodeVersion":"26.4.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jsdom":"^30.0.1","vitest":"^3.2.7","typescript":"^5.6.0","@types/node":"^22.0.0","@anjunar/scalajs-ui":"*","@anjunar/scalajs-ui-core":"*","@anjunar/scalajs-ui-bridge":"*"},"peerDependencies":{"@anjunar/scalajs-ui":"^1.1.0","@anjunar/scalajs-ui-core":"^1.1.0","@anjunar/scalajs-ui-bridge":"^1.1.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/scalajs-ui-controls_1.1.0_1790707608836_0.02234188592732811"}}},"time":{"created":"2026-09-11T16:14:05.130Z","modified":"2026-09-29T18:46:49.243Z","1.0.0":"2026-09-11T16:14:05.501Z","1.0.2":"2026-09-15T21:00:26.512Z","1.0.3":"2026-09-16T10:01:40.048Z","1.0.5":"2026-09-18T12:55:40.664Z","1.0.6":"2026-09-21T02:43:12.490Z","1.1.0":"2026-09-29T18:46:48.936Z"},"bugs":{"url":"https://github.com/anjunar/scalajs-ui/issues"},"author":{"name":"Patrick Bittner"},"license":"MIT","homepage":"https://github.com/anjunar/scalajs-ui#readme","repository":{"url":"git+https://github.com/anjunar/scalajs-ui.git","type":"git","directory":"npm/scalajs-ui-controls"},"description":"The TypeScript controls API of Scala JS UI 1.0: tabs, carousel, and the virtualized table / data grid / list, projected onto the ui.control components that run in the Scala.js runtime published as @anjunar/scalajs-ui-bridge.","maintainers":[{"name":"anjunar","email":"anjunar@gmx.de"}],"readme":"# @anjunar/scalajs-ui-controls\n\nTyped Scala JS UI 1.0 controls for tabs, carousels, and virtualized table, data-grid, and list views.\n\n## Overview\n\nThis package wraps `ui.control` components. Virtualization, row measurement, sorting, remote range loading, carousel timers, and SSR/hydration behavior are implemented by the Scala.js runtime, not by a second TypeScript control library.\n\n## Installation\n\n```bash\nnpm install @anjunar/scalajs-ui-core @anjunar/scalajs-ui-controls @anjunar/scalajs-ui-bridge @anjunar/scalajs-ui\n```\n\nUsing only `@anjunar/scalajs-ui-controls` (no router/forms/editor)? Import `@anjunar/scalajs-ui-bridge/controls` instead of the bare package to skip installing the other features -- see that package's README.\n\n## Quick start\n\n```ts\nimport { listProperty, text } from \"@anjunar/scalajs-ui-core\";\nimport { column, tableView } from \"@anjunar/scalajs-ui-controls\";\n\ntype Book = { title: string; year: number };\nconst books = listProperty<Book>([\n  { title: \"A\", year: 2024 },\n  { title: \"B\", year: 2025 },\n]);\n\ntableView(books, [\n  column(\"Title\", (book) => text(book.title), { prefWidth: 280, sortable: true, sortKey: \"title\" }),\n  column(\"Year\", (book) => text(String(book.year))),\n], { rowHeight: 44 });\n```\n\n## Usage\n\n### Column widths and resizing\n\nColumns accept `minWidth` (default 40), `prefWidth` (160), `maxWidth` (unbounded),\nand reactive `resizable` (true). Drag the grip at the inline end of a header, or focus it and\npress Left/Right (10 px; Shift: 1 px). Resizing does not sort the column or rebuild its cells.\nPointer-up/cancel, lost capture, window blur, hiding/removing the column, locking it, changing\nthe policy and unmount all end the gesture. Cancellation retains the last applied width.\n\n```ts\nconst policy = property<ColumnResizePolicy>(\"flex-last-column\");\nconst table = tableView(books, [\n  valueColumn(\"Title\", book => book.title, { minWidth: 140, prefWidth: 280, maxWidth: 900 }),\n  valueColumn(\"Year\", book => book.year, { minWidth: 70, prefWidth: 100, maxWidth: 300 }),\n], { columnResizePolicy: policy });\ntable.resizeColumn(0, 40); // Visible-column index, pixel delta; true if any change was applied.\nconst widths: readonly number[] = table.columnWidths.get;\n```\n\n`ColumnResizePolicy` is exported by `@anjunar/scalajs-ui-controls`. Available policies:\n\n| Policy | Compensation for a resized column |\n| --- | --- |\n| `unconstrained` | None; horizontal scrolling works in both paging and scrolling modes. |\n| `all-columns` | Proportionally across all other resizable columns. |\n| `subsequent-columns` | Proportionally across following resizable columns. |\n| `next-column` / `last-column` | Only the next / last following column. |\n| `flex-next-column` / `flex-last-column` | Following columns in forward / reverse order, continuing at limits. |\n\nConstrained policies fit the viewport and suppress horizontal scrolling; impossible bounds\nleave unused space or clipped columns. Locked columns do not participate in compensation.\nThe default is `flex-last-column`, preserving UI's existing fit-to-width layout. Automatic\nviewport fitting uses bounded proportional distribution; policies govern user/API deltas.\n\nA group column has no width of its own -- only its visible leaves do -- so it gets a resize\nhandle too: dragging it shares the delta across its own children proportionally to their\ncurrent width first, and only whatever they collectively cannot absorb compensates columns\noutside the group, through the same policy as any other resize. `table.resizeColumn` has no\nprogrammatic way to target a group (`visibleColumnIndex` only addresses leaves); the handle\nitself needs no JavaScript API for this, since it holds the Scala column reference directly.\n\n```ts\nconst customResizePolicy: CustomColumnResizePolicy = (request) => {\n  const next = request.widths.map((width, index) =>\n    request.targetIndices?.includes(index) ? width + request.targetDelta! : width);\n  return next.map(width => Math.round(width / 20) * 20); // snap every column to a 20px grid\n};\nconst table = tableView(books, columns, { customResizePolicy });\n```\n\n`customResizePolicy` replaces `columnResizePolicy` and all seven built-in strategies at once,\nfor both re-layout (`targetIndices`/`targetDelta` absent) and every resize (present; more than\none index for a group). It must return an array the same length as `request.columns`; anything\nelse is rejected atomically and the previous widths are kept, and each accepted width is still\nclamped to its own column's bounds -- a policy cannot violate them by construction. Not\nreactive: set once when the table is created, like `row`/`rowKey`. Scala exposes the same\ncontract as `TableView.customResizePolicyProperty: Property[Option[CustomColumnResizePolicy]]`\nand the DSL setter `customResizePolicy = request => ...`; `.set(None)` restores\n`columnResizePolicy`.\n\nUser widths are separate from `prefWidth` and survive measurements and hide/show. A Scala\npreferred-width change resets that column's user override; detaching it discards its override.\nScala exposes the same model via `minWidthProperty`, `maxWidthProperty`, `resizableProperty`,\nread-only `widthProperty`, `columnResizePolicyProperty` and `resizeColumn(column, delta)`.\nHidden/detached columns report bounded preferred width through `widthProperty`; the table's\nwidth snapshot only includes visible columns. Defaults remain UI-specific (40/160 px).\nNegative minima normalize to zero, invalid non-finite minima/preferences use their defaults,\nnon-finite maxima are unbounded, and a conflicting minimum takes precedence over the maximum.\nInvalid resize deltas/indices, hidden/foreign columns and disposed tables are no-ops.\n\nDouble-click a resize grip, or press Enter on its focus, to fit the column's content.\nThe same command is `table.autoFitColumn(visibleColumnIndex)` in TypeScript and\n`table.autoFitColumn(column)` in Scala. It measures the header (including current sort\ndecoration) and at most 100 mounted, loaded cells, ordered by absolute row index.\nOverscan cells may participate; unloaded/off-window data is not fetched or rendered.\nThe maximum intrinsic CSS width is rounded up, then applied through the existing resize\npolicy and min/max bounds. Constrained policies may only fit partially, or not at all\nwhen no compensating column has capacity. The command can also shrink a column.\n\nMeasurement uses temporary max-content sizing on the existing header/cell hosts and\nrestores their width constraints synchronously. There are no clones or renderer calls;\ncustom controls keep their DOM, bindings, focus and text selection. Content keeps its CSS\nlayout semantics (e.g. explicit child widths); this is not a text-only width estimate.\nCall again after fonts/images load or content changes if another fit is desired.\n\nAuto-fit is browser-only; during hydration the latest valid request waits for claiming.\nInvalid/hidden/locked columns, unmeasurable layout, protected hosts and active native\ncomposition are no-ops, not queued retries. A true result means a width changed or a\nhydration-time request was accepted; SSR and disposed handles return false.\n\n### Column classes\n\nA column's own `AbstractComponent`/DOM facilities do not reach its header cell or its data\ncells: the header is a separately created component (`TableView.HeaderSlot`), and each data\ncell is its own `TableCell` instance. `headerClass`/`cellClass` are the explicit hook:\n\n```ts\nconst numericColumn = valueColumn(\"Year\", book => book.year, {\n  headerClass: [\"numeric-header\"],\n  cellClass: [\"numeric-cell\"],\n});\n```\n\n```css\n.numeric-header, .numeric-cell { justify-content: flex-end; text-align: right; }\n```\n\nBoth are reactive and additive: they sit alongside the framework's own `ui-table-header-cell`/\n`ui-table-cell` classes rather than replacing them, and changing the array adds/removes exactly\nthe classes that changed. `cellClass` applies to every data cell of that column, not just the\nfirst. Scala exposes `headerClassesProperty`/`cellClassesProperty` and the DSL setters\n`headerClasses`/`cellClasses`, taking a `Seq[String]` or a bindable `ReadOnlyProperty[Seq[String]]`.\n\n### Column header content\n\n`headerCell` replaces a column's default header text, and `sortIndicator` replaces its default\nCSS-only sort arrow, with composed content:\n\n```ts\nconst titleColumn = valueColumn(\"Title\", book => book.title, {\n  sortable: true, sortKey: \"title\",\n  headerCell: () => {\n    div(() => { style(\"display\", \"flex\"); style(\"gap\", \"6px\"); text(\"📖\"); text(\"Title\"); });\n  },\n  sortIndicator: state => {\n    div(() => {\n      classes(\"sort-pill\");\n      text(state.map(s => s.sorted ? `${s.ascending ? \"▲\" : \"▼\"} ${s.priority}` : \"\"));\n    });\n  },\n});\n```\n\n`sortIndicator` receives a `ReadOnlyProperty<SortIndicatorState>` (`{ sorted, ascending,\npriority }`, one-based) that updates as the column's own remote-sort state changes; nothing else\nabout `headerClass`/`cellClass` or the header's built-in classes changes. Omitting `sortIndicator`\nkeeps the default arrow even when `headerCell` is set. Scala exposes the same slots as\n`TableColumn.headerCell { ... }` and `TableColumn.sortIndicator { state => ... }` in the column DSL.\n\nA header right-click/context menu has no dedicated API either, for the same reason: `headerCell`\nis real composition, so it reaches the same anchor-following `overlay` any other dropdown/popover\nuses (a `ComboBox`'s own dropdown, or the built-in column-visibility menu), gated by an ordinary\nboolean the way `when()` gates any other conditional content -- not a second popup engine.\n\n```ts\nconst menuOpen = property(false);\nconst titleVisible = property(true);\ncolumn(\"Title\", book => book.title, {\n  visible: titleVisible,\n  headerCell: () => {\n    div(() => {\n      text(\"Title\");\n      on(\"contextmenu\", event => { event.preventDefault(); menuOpen.set(true); });\n    });\n    when(menuOpen, () => {\n      overlay({ widthPx: 200 }, () => {\n        div(() => {\n          classes(\"header-menu\");\n          on(\"keydown\", event => { if (event.key === \"Escape\") menuOpen.set(false); });\n          button(\"Hide column\", {}, () => onClick(() => { titleVisible.set(false); menuOpen.set(false); }));\n        });\n      });\n    });\n  },\n});\n```\n\n### Remote multi-column sorting\n\nClick a sortable header to cycle ascending, descending, unsorted. Shift-click keeps other\nterms and their priorities, appends a new term, or changes/removes that term in place.\nFocused headers support Enter/Space with the same Shift modifier; resize grips and reorder\ngestures retain their own commands. Arrows include the one-based priority. `aria-sort` is\non the primary header only; other sorted headers describe their direction and priority.\n\n`table.toggleSort(visibleColumnIndex, additive?)` and `table.clearSort()` use the same\nremote command path, resetting paging and scrolling to the start. Scala takes a column\ninstance instead of an index. Commands are inert on local sources, invalid/hidden/unsortable\ncolumns, protected hosts, active IME composition, during SSR/hydration and after disposal.\nHiding or moving a sorted column does not alter the remote query. `sorting` (Scala:\n`sortingProperty`) exposes the remote source's **requested** descriptors. A true command\nresult means a request was issued, not that its load succeeded; the existing loading/error\nstate and accepted-reset selection rules still apply. Transactional sort rollback and\ncustom sort-policy/events are not provided by this increment.\n\nThe control never sorts/filters cached rows. Supply `sortKey`/`sortable` and a remote\n`sortQuery` handling all descriptors in order. The demos simulate that backend in their\nloaders; production applications do it through their RemoteDataList/server query.\n\nSet an entire order explicitly, without cycling headers or issuing intermediate requests:\n\n```ts\ntable.setSortOrder([\n  { columnIndex: 1, ascending: true },\n  { columnIndex: 2, ascending: false },\n]);\ntable.sort(); // Reissue the current requested order, e.g. retry after a load error.\n```\n\n`TableSort` is the exported TypeScript term type. Indices refer to **currently visible**\ncolumns at call time; they resolve immediately to remote field keys, which survive later\nreordering. Scala uses `Seq(TableSort(authorColumn), TableSort(yearColumn, false))` with\ncolumn identities instead of indices. Validation is atomic: foreign/hidden/disposed or\nunsortable columns, missing/blank keys and duplicate keys reject the entire order without\nchanging sorting, selection, paging or scrolling. JavaScript inputs also require finite\ninteger indices and real boolean directions. An empty sequence requests unsorted data.\n\nSetting the same order again or calling `sort()` asks the source to reload; the source may\ndeduplicate an identical in-flight request. `sort()` preserves the current descriptors,\nincluding terms whose columns have since been hidden or keys supplied directly by the\nsource. Both commands obey the browser/hydration/lifetime guards above. They do not\npromise successful completion or implement sort rollback. A failed request retains the\nexisting rows/selection/focus; only an accepted replacement invalidates selection/focus.\n\n### Column visibility menu\n\nSet `tableMenuButtonVisible: true` to show a Columns button above the header. The table\nmust be inside `viewport(...)` from `@anjunar/scalajs-ui-viewport` when this option is enabled.\nThe existing Viewport owns the overlay and anchor positioning; the table owns its lifetime.\nNo native popover or second overlay implementation is involved. Without the option,\ntables still work without a viewport. `showHeader: false` also hides the menu button.\n\nThe menu lists all group and leaf columns in tree order, including hidden columns. It stays open\nwhen toggling a checkbox and remains usable after hiding every column. Existing selection,\nsorting and user widths are preserved. Hiding a column disposes its cells, as with `visible`.\nArrow keys/Home/End navigate, Enter/Space toggle, Escape closes and returns focus. Tab closes\nand continues navigation from the trigger. Outside pointer/focus, window blur, structural\ncolumn changes, hiding the menu/header and unmount close and clean up the overlay/listeners.\n\n`tableMenuButtonVisible` and `columnMenuText` are reactive; the latter supplies the trigger\ntext and accessible menu label. Scala exposes corresponding properties and DSL setters.\nSSR renders only the disabled trigger; hydration enables it without opening or taking focus.\nThe trigger is disabled if the table has no columns. Visibility changes from menu actions\nrespect active native composition and protected table hosts.\n\n`visible` is a one-way input. To keep application-owned state in sync with menu actions,\nuse `onVisibilityChange`, called on changes but not for the initial value:\n\n```ts\nconst visible = property(true);\n// Inside viewport(() => ...):\ntableView(books, [valueColumn(\"Title\", book => book.title, {\n  visible,\n  onVisibilityChange: next => visible.set(next),\n})], { tableMenuButtonVisible: true, columnMenuText: \"Spalten\" });\n```\n\n### Column reordering\n\nDrag a header (not its resize grip) to an insertion marker. Alternatively, focus the header\nand press Alt+Shift+Left/Right. `reorderable: false` (also reactive) disables these gestures,\nbut not `table.moveColumn(fromVisibleIndex, toVisibleIndex)` or Scala column-list changes.\nIndices refer to the current visible order; the destination is the final index, not a boundary.\nHidden columns keep their relative order. Moves change the canonical column list, preserving\nmounted cells, bindings, focus/text selection, row selection, sorting and width overrides.\n\n`moveColumn` is browser-only; SSR keeps the declared order and hydration applies the latest\nvalid command after claiming. Invalid/no-op requests, disposed tables, protected hosts and\nactive IME composition return false. Scala uses `moveColumn(column, toVisibleIndex)`.\nNative composition is not interrupted to perform a move; retry after composition ends.\nEscape, pointer cancellation/capture loss, blur, hiding/locking/removing the dragged column\nor disposal cancel a drag. Dropping outside the header cancels too; drag release never sorts.\nWithin a column group, leaf columns can be moved among their siblings. A move across group\nboundaries is rejected so it cannot silently change the declared hierarchy. Group-header drag,\nmoving an entire group and drag-edge autoscrolling are not implemented yet.\n\n### Column groups\n\n`columnGroup` nests columns to any depth. Groups render one header row per level and consume\nthe combined width of their currently visible leaf columns. Only leaves render cells, appear in\n`columnWidths`, count in `visibleColumnCount`, and participate in index-based table operations.\nTop-level leaves span the remaining header rows. A group's own header still has a resize handle\n(see \"Column widths and resizing\" above): dragging it resizes every one of its visible leaves\ntogether.\n\n```ts\ntableView(books, [\n  columnGroup(\"Book\", [\n    valueColumn(\"Title\", book => book.title),\n    valueColumn(\"Author\", book => book.author),\n  ]),\n  columnGroup(\"Details\", [\n    valueColumn(\"Year\", book => book.year),\n    column(\"Note\", book => text(book.note)),\n  ]),\n]);\n```\n\nIn Scala, `columnGroup(\"Book\") { column(...); column(...) }` builds the same tree.\n`TableColumn.columns`, `parentColumnProperty` and `tableViewProperty` expose its model.\nChild/root mutations validate duplicates, cycles, disposed columns and foreign ownership before\nobservers see the new forest. Removed subtrees stay reusable until their owning table is disposed.\nVisibility on a group suppresses all descendants without changing their individual flags.\n\nThe handle supplies `visibleColumnCount`, `getCellData(rowIndex, visibleColumnIndex)` and\n`getCellObservableValue(...)`. Coordinates use absolute rows and the current visible leaf order;\ninvalid, hidden, unloaded or non-value cells return `null`.\n\n### Observed table values\n\n`column(text, cell)` still composes arbitrary row content, including embedded editors.\n`valueColumn` adds a typed snapshot/property accessor and an optional renderer over the\nobserved cell value:\n\n```ts\nimport { listProperty, property, text } from \"@anjunar/scalajs-ui-core\";\nimport { tableView, valueColumn } from \"@anjunar/scalajs-ui-controls\";\n\nconst person = { name: property(\"Ada\"), year: 1815 };\ntableView(listProperty([person]), [\n  valueColumn(\"Name\", (row) => row.name),\n  valueColumn(\"Greeting\", (row) => row.name, {\n    cell: (value) => text(value.map((name) => `Hello ${name ?? \"\"}`)),\n  }),\n  valueColumn(\"Year\", (row) => row.year),\n]);\nperson.name.set(\"Grace\"); // Updates both observed cells without recomposing them.\n```\n\nSnapshots do not observe mutations. Custom value renderers receive a read-only property;\nediting still requires the row's writable property or an explicit application callback.\n\n### Standard editable cells\n\nThe standard column helpers use the same observed-value and edit-event contract as `valueColumn`,\nwhile supplying the corresponding cell component:\n\n```ts\nconst owners = [{ id: 1, name: \"Ada\" }, { id: 2, name: \"Grace\" }];\nconst person = {\n  name: property(\"Ada\"),\n  active: property(true),\n  role: property(\"author\"),\n  owner: property(owners[0]),\n  progress: property(0.65),\n  age: property(42),\n};\nviewport(() => {\n  tableView(listProperty([person]), [\n    textFieldColumn(\"Name\", row => row.name),\n    convertingTextFieldColumn(\"Age\", row => row.age, text => {\n      const value = Number(text);\n      return Number.isInteger(value) && value >= 0\n        ? { ok: true, value }\n        : { ok: false, error: \"Whole number required\" };\n    }),\n    checkBoxColumn(\"Active\", row => row.active),\n    choiceBoxColumn(\"Role\", row => row.role, [\"author\", \"editor\"]),\n    comboBoxColumn(\"Owner\", row => row.owner, owners, {\n      converter: owner => owner.name,\n      identityBy: owner => owner.id,\n    }),\n    progressBarColumn(\"Progress\", row => row.progress),\n  ], { editable: true, cellSelectionEnabled: true });\n});\n```\n\nA text editor opens with double-click, F2 or Enter on the logically focused cell. Enter commits,\nEscape cancels, and Tab/Shift+Tab commits before moving logical focus to the next/previous visible\ncell in row-major order. Tab does not automatically open another edit session. IME composition\nkeeps these keys inside the input. Losing DOM focus keeps the session open by default; set\n`editOnBlur: \"commit\"` or `\"cancel\"` when the application needs another explicit policy.\n\n`convertingTextFieldColumn` keeps the table draft strongly typed. Its parser returns either\n`{ ok: true, value }` or `{ ok: false, error }`; invalid raw text stays in the input without\nchanging the typed draft or row property. Enter, Tab and `editOnBlur: \"commit\"` leave an invalid\nsession open. The visible message is linked from the input with `aria-errormessage`, and disappears\nas soon as parsing succeeds. A Scala column uses the equivalent `convertingTextFieldCell` helper\nwith `String => Either[String, T]`.\n\nA checkbox remains visible and commits each click or Space toggle as one start/commit operation.\nF2/Enter can also enter it as the focused editor; Escape cancels that open session and Enter\ntoggles it. Both cells obey table, ancestor-column, column and cell editability. A writable\n`Property` receives the default commit; `editCommitHandler` remains the alternative for read-only\nor application-managed values.\n\n`choiceBoxColumn` uses a compact native select. `comboBoxColumn` uses the Forms ComboBox and must be\ninside a `viewport`, because its item list is rendered through the shared overlay implementation.\nThe first Escape closes an open ComboBox popup; the next cancels the table edit. Popup focus does\nnot finish the edit. Both helpers accept a fixed array or live `ListProperty`, plus typed\n`converter` and `identityBy` callbacks. `progressBarColumn` is always read-only and clamps its input\nto the 0–1 range while exposing 0–100 through ARIA.\nCells at overlapping absolute row positions retain their component/DOM identity during\nscrolling and measurement when the item instance is unchanged. Leaving the virtual window,\nreplacing an item, resetting the source list, or changing the renderer can recreate cells.\nIdentity across arbitrary data reordering without `rowKey` is not preserved.\n\n### Column visibility and refresh\n\nColumn `visible` accepts a boolean or a `ReadOnlyProperty<boolean>` (default: true).\nHidden columns do not render headers/cells or consume width. Their definitions remain\nattached; showing them again creates fresh cells. Other visible cells remain mounted when\na neighboring column is hidden or shown. With no visible columns, the table displays its\nplaceholder instead of rows. Hiding a group suppresses its whole subtree; showing it restores\nthe descendant visibility state.\n\n```ts\nconst showYear = property(true);\nconst book = { title: \"Dune\", year: 1965 };\nconst table = tableView(listProperty([book]), [\n  valueColumn(\"Title\", (row) => row.title),\n  valueColumn(\"Year\", (row) => row.year, { visible: showYear }),\n]);\nshowYear.set(false);\nbook.title = \"Solaris\";\ntable.refresh(); // Re-evaluates visible snapshots and cell bodies; no remote reload.\n```\n\n`tableView()` returns `TableViewHandle<T>` with `refresh()` and read-only `isDisposed`,\nplus the selection and logical row/cell-focus APIs below.\nAfter unmount, refresh is a no-op. Refresh recreates visible cell content and can reset local\neditor state, so use observed values for live edits. Calling code may ignore the new return\nvalue. An explicitly void-returning expression arrow should use a block:\n`() : void => { tableView(source, columns); }`. The matching linked bridge must be installed.\n\n### Single selection\n\nThe handle exposes read-only `selectedIndex: ReadOnlyProperty<number>` and\n`selectedItem: ReadOnlyProperty<T | null>`. Both read from one coherent state:\n\n```ts\ntable.selectIndex(0);\ntext(table.selectedItem.map((selected) => selected?.title ?? \"No selection\"));\ntable.selectItem(book); // First equal loaded item; does not fetch missing rows.\ntable.clearSelection(); // selectedIndex = -1, selectedItem = null.\n```\n\nLocal inserts/removals move the selection with its occurrence, including duplicates.\nRemoving or replacing that occurrence clears it; an explicit item update retains the\nindex and adopts the updated item. A list reset retains selection only when the same\nobject instance occurs exactly once in the new list, not merely an equal new object.\n\nRemote indices are absolute. Loading a gap does not shift selection. Selecting a valid\nunloaded position yields a null item until it loads. Accepted reload/sort replacements\nclear selection; an in-flight or failed replacement leaves the previous selection intact.\nInvalid indices (including fractions, NaN, and infinity) clear selection. `selectItem`\nmay scan the entire index space, so prefer a known index for large remote sources.\n\nAfter unmount, selection operations are no-ops. Reactive `text` bindings follow normal\ncomponent lifecycle; dispose subscriptions that you create manually with `observe`.\nDerived properties may notify even when only the other selection field changes.\nCell selection is not available yet; logical row/cell focus is described below.\nSelection identity does not guarantee DOM/editor identity across data reordering.\n\n### Multiple selection\n\nPass `selectionMode: \"multiple\"` (or a reactive property) to `tableView`. The default\nremains `\"single\"`. The same runtime-owned handle exposes both modes:\n\n```ts\nconst table = tableView(books, [valueColumn(\"Title\", (book) => book.title)], {\n  selectionMode: \"multiple\",\n});\ntable.selectIndices([0, 2]);\ntable.selectRange(3, 6); // Adds 3, 4, 5; reversed ranges also exclude the end.\ntext(table.selectedIndices.map((indices) => `${indices.length} selected`));\ntable.clearIndex(2);\ntable.clearAndSelect(0); // Replace the entire selection in one update.\n```\n\n`selectedIndices` is a read-only property of sorted, unique absolute indices;\n`selectedItems` contains their loaded items in index order. `selectedIndex`/`selectedItem`\ndescribe the **lead** (last valid selection), not the whole result. Removing the lead\nchooses the largest remaining selected index. All properties read from one coherent\nsnapshot. Returned arrays cannot mutate the model (item objects remain application-owned).\n\nIn multiple mode, `selectIndex`/`selectItem` add to selection. `selectIndices` ignores invalid\nand duplicate values; ranges clip to valid positions. `clearIndex` ignores invalid positions.\nNon-integral/NaN/infinite range boundaries are ignored. Invalid `selectIndex` and\n`clearAndSelect` still clear selection for compatibility with the original single API.\n`selectAll` works only in multiple mode. `selectFirst`, `selectLast`, `selectNext` and\n`selectPrevious` operate on the lead and do not move DOM focus or scroll the viewport.\n\nPlain click replaces selection. Ctrl/Cmd-click toggles a row; Shift-click replaces it with\nthe inclusive anchor-to-click range; Ctrl/Cmd+Shift adds that range. Repeated Shift-clicks\nkeep the anchor, including after index rebasing. Loaded custom rows use the same membership\nstate. Switching to single via `setSelectionMode(\"single\")` retains just the lead.\nThe reactive option is one-way: handle calls do not write back to the supplied property.\n\nFor sparse remote data, selected indices may outnumber selected items: **do not zip the\ntwo lists**. Selection never fetches missing values. `selectAll` explicitly selects the\ncurrently known index space and uses memory proportional to its size; it is not a\nserver-side all-results token and does not expand automatically when the source grows.\nAccepted reloads clear the selection; structural deltas preserve surviving occurrences.\nReplaceable selection models, cell selection and full grid accessibility remain separate work.\n\n### Disabled rows\n\n```ts\ntableView(books, columns, { rowDisabled: book => book.archived });\n```\n\nA disabled row and its cells stay visible and keyboard-reachable -- disabled is not the same as\nabsent -- but cannot be selected, edited, or fire the row double-click event; the same guard\ncovers mouse, keyboard and cell-selection paths, in one place (`TableSelectionModel.click`/\n`clickCell` and `TableView.canStartEdit`, not one check per input path). Projected as\n`ui-table-row-disabled`/`ui-table-cell-disabled` and `aria-disabled`. Not reactive as a whole\nfunction -- `rowDisabled` itself is read once -- but each row re-evaluates it whenever that row's\nown bound value changes, the same as any other per-row derived state.\n\nA custom `row` callback reads the same state through `row.disabled`, alongside `row.selected` and\n`row.focused`. Programmatic selection (`selectIndex`, `clearAndSelect`, ...) is untouched: disabled\nblocks user interaction through the widget, not deliberate application control -- the same\ndistinction JavaFX's `Node.disable` draws for event dispatch versus direct API calls. There is no\nindependent per-cell predicate; a cell simply inherits its row's disabled state, matching how\n`Node.disable` cascades from a disabled parent to its children.\n\nA column-wide style attribute beyond `headerClass`/`cellClass` (JavaFX's own raw-CSS-string\n`TableColumnBase.styleProperty`) is deliberately not part of this API either, for the same reason\nC10 gave for skipping it: the class list is the idiomatic hook in this framework's convention.\n\n### Logical row and cell focus\n\nFocus is independent of selection. `focusedIndex` and `focusedItem` describe the logical row;\n`focusedCell` adds an absolute row and the current visible-leaf column index. Row-only focus uses\n`column: -1`. A stable runtime column reference keeps cell focus on the same leaf when columns are\nreordered; hiding or removing that leaf degrades the coordinate to row-only focus.\n\n```ts\ntable.focusCell(4, 1);\ntext(table.focusedCell.map(position =>\n  position === null ? \"none\" : `${position.row}:${position.column}`));\ntable.focusRightCell();\ntable.focusBelowCell();\n```\n\n`focusIndex`, `focusCell`, `focusNext`/`focusPrevious`, and the four cell-direction methods mutate\nonly logical focus. They do not select, scroll, take DOM focus, or fetch remote gaps. Invalid\ncoordinates clear focus. Up/Down keyboard navigation retains an active leaf; Left/Right enters or\nmoves cell focus and scrolls that leaf into view. Movement stops at table edges. Clicking a normal\ncell selects its row and focuses that cell, while inputs, buttons, links, and other interactive\ndescendants retain their own mouse and keyboard behavior.\n\nWhen the grid owns DOM focus, `aria-activedescendant` points to the mounted focused cell (or row for\nrow-only focus). Offscreen coordinates never leave a dangling ARIA reference. Cell focus survives\nrow rebasing and column reordering, follows `rowKey` on accepted replacements, and remains a valid\ncoordinate for known unloaded remote rows. A replaceable focus model and cell selection remain\nseparate work.\n\n### Custom rows\n\n`TableViewOptions<T>.row` replaces the content of each row. Its callback runs in that\nrow's component scope: styles, attributes, events and `disposeWith` belong to the row.\nCall `renderCells()` to include the usual columns, or omit it for entirely custom content:\n\n```ts\nimport { attr, style } from \"@anjunar/scalajs-ui-core\";\n\ntableView(books, [valueColumn(\"Title\", (book) => book.title)], {\n  row: (row) => {\n    if (!row.empty.get) attr(\"title\", row.item.get?.title ?? \"\");\n    style(\"font-weight\", row.selected.map((selected) => selected ? \"600\" : \"400\"));\n    row.renderCells();\n  },\n});\n```\n\nFor remote reloads or local list resets that replace object instances, provide a stable and unique\n`rowKey` when selection and logical focus should follow the same entities:\n\n```ts\ntableView(books, [valueColumn(\"Title\", book => book.title)], {\n  rowKey: book => book.id,\n});\n```\n\nKey restoration examines values already loaded in the new result. It does not fetch sparse remote\npositions, and duplicate or failing keys are cleared rather than matched ambiguously. Without a\n`rowKey`, accepted remote replacements continue to clear selection and focus.\n\n`onScrollTo` and `onScrollToColumn` observe successful programmatic navigation after its request\nreaches a measurable browser viewport. They do not run for invalid, hidden, SSR, superseded, or\ndisposed requests. Column indices use the visible order at execution time.\n\n`TableRowContext<T>` exposes read-only `item`, absolute `index`, `empty`, `selected`, `focused`\nand `disabled` properties. Remote placeholders also receive the callback, with `empty = true` and\n`item = null`; they remain non-interactive and unselected until replaced by loaded rows.\nA loaded null value is distinguishable by `empty = false`. Row selection, standard classes\nand cleanup remain runtime-owned even when standard cells are omitted.\n\n`renderCells()` may be called at most once, synchronously in the row callback or inside\na nested element built by that callback. Give custom wrappers the appropriate layout\n(for example `display: flex`) to retain column alignment. Deferred calls are rejected.\nThe callback does not rerun on selection changes: use the supplied reactive properties.\nIt does run for newly materialized rows and on `refresh()`. Refresh now recreates whole\nvisible rows, including custom snapshot content; it may discard local editor state.\nOverlapping scroll slots with unchanged items retain their row instances. Rows are fixed at\n`rowHeight` by default. Set `variableRowHeight: true` to let visible rows grow with their content:\n\n```ts\ntableView(books, columns, { rowHeight: 32, variableRowHeight: true });\n```\n\nIn this mode, `rowHeight` is the minimum height and the estimate for rows not measured yet.\nThe browser measures mounted rows after hydration and observes later content or width changes.\nMeasurements above the viewport adjust scroll position so the visible row stays anchored.\nThe option accepts a reactive boolean; switching modes retains the visible row position. On the\nserver rows start from the same deterministic minimum. This does not imply automatic cell spanning.\n\n### Other controls\n\n`tabs` accepts `tab(title, body)` definitions and supports `active-only` or `keep-mounted` rendering. `carousel` accepts a list property and a slide renderer; `autoAdvanceMs` controls browser auto-advance. `tableView`, `dataGrid`, and `virtualList` accept a local `ListProperty` or a `RemoteSource`.\n\n```ts\nimport { remoteSource, virtualList } from \"@anjunar/scalajs-ui-controls\";\n\ntype Item = { title: string };\ntype Query = { offset: number; limit: number };\nconst firstPage: readonly Item[] = [{ title: \"First item\" }];\n\nconst source = remoteSource<Item, Query>({\n  initialQuery: { offset: 0, limit: 50 },\n  initial: firstPage,\n  initialOffset: 0,\n  totalCount: 1000,\n  rangeQuery: (query, offset, limit) => ({ ...query, offset, limit }),\n  load: async (query) => ({ items: firstPage, offset: query.offset, totalCount: 1000 }),\n});\n\nvirtualList(source, (item, index) => text(item === null ? `Loading ${index}` : item.title));\n```\n\n`crawlable` and `crawlId` render a deterministic server slice with ordinary pager links.\nTable selection, logical row focus, navigation and refresh are available through its handle;\ndata-grid/list selection handles are not projected by this facade yet.\n\n### Row focus and keyboard navigation\n\n`table.focusIndex(index)`, `focusNext()` and `focusPrevious()` update `focusedIndex` and\n`focusedItem` independently of selection. These are logical model operations: they do not scroll,\nfetch remote data or take DOM focus. Invalid indices clear focus (`-1`/null); unloaded known\npositions have an index and null item until loaded. Previous at the beginning does nothing;\nnext from no focus chooses the first row. Operations are inert after disposal.\n\nThe grid is a native tab stop. On focus it restores logical focus, or starts at the selected row /\nfirst row. A background row click focuses the grid and selects the row. Embedded inputs, buttons,\nlinks, contenteditable and other focusable controls keep their own interaction; header, resize\nand menu controls also retain their own keys. Logical focus survives leaving the grid, but the\nrow outline is shown only while the grid itself has DOM focus.\n\n- Up/Down, Home/End, PageUp/PageDown move focus, select the destination and reveal it.\n- Ctrl/Cmd with navigation moves focus without changing selection.\n- Shift extends the anchored selection; Ctrl/Cmd+Shift adds the range to the selection.\n- Space selects; Ctrl/Cmd+Space toggles. Ctrl/Cmd+A selects all in multiple-selection mode.\n- Tab retains native navigation. Alt-modified, canceled and IME-composition keys are ignored.\n\nPage movement uses the measured viewport height with one row of overlap (minimum one row).\nPaging remains paging; remote targets load through normal row navigation. Structural list edits\nrebase focused occurrences; removal clears focus. A reset preserves only unique reference identity;\nan accepted remote query replacement clears focus. `row.focused` exposes logical row state to\ncustom renderers. Native focus stays on the grid as virtual rows enter/leave; `aria-activedescendant`\nonly references mounted rows, with browser IDs installed after hydration without stealing focus.\nGrid/row/cell/header roles, row/column counts and indices accompany this first accessibility layer.\nCell focus/selection, replaceable focus models, sort announcements and full screenreader/IME\nacceptance are still pending. Runtime-managed row IDs are reserved for active-descendant ownership.\n\n`table.scrollToIndex(499)` reveals the 500th row in absolute view coordinates;\n`table.scrollToItem(item)` reveals the first matching loaded item. Neither changes selection,\nDOM focus or the display mode. In paging mode the containing page is shown; if that page is\ntaller than the viewport, its rows can also be revealed programmatically. In scrolling mode,\nalready visible rows stay in place. Content headers are included and the last row is clamped\nto the end of the content.\n\nInvalid/unknown indices and absent items are ignored. A known but unloaded remote position\nuses the existing range loader (including its loading/error/retry behavior); item lookup never\nfetches missing items. Sources without random range access retain their sequential loading\nbehavior. These methods do not discover unknown positions beyond the source's current extent.\n\nNavigation is browser-only: SSR keeps its deterministic slice. Requests made during\ncomposition/hydration or while the viewport has no height/visible columns wait until it can be\nmeasured. The latest valid request wins, is revalidated against the current extent, and\nsupersedes the initial cookie/URL scroll restoration. Disposal cancels pending navigation.\nThere is no load-completion promise. `onScrollTo` runs once after a valid request is actually\napplied; invalid, superseded, SSR and disposed requests remain silent.\n\n`table.scrollToColumnIndex(index)` reveals a column in the current **visible** order with minimal\nhorizontal movement. Oversized columns align at their start. Hidden columns contribute neither\nindices nor width. The operation uses current rendered widths after resizing/reordering, leaves\nvertical position, selection, focus and editors untouched, and does not load remote rows. It also\nworks with no rows or a hidden header. Widths and the resize policy are not changed: usually use\nfree (`unconstrained`) widths when horizontal scrolling is desired.\n\nColumn navigation is browser-only too. The last valid request waits for hydration or a non-zero\nviewport width and remembers the column instance, not an index that could change during reordering.\nThe target is revalidated before scrolling; hidden/removed targets and disposed tables are ignored.\nThe header offset is synchronized immediately, including browser clamping. Row and column requests\nare independent. Scala additionally accepts a column reference via `scrollToColumn(column)`;\n`onScrollToColumn` reports the applied column (as its current visible index in TypeScript).\n\nSet the reactive `direction` option to `\"rtl\"` to put visible column zero at the right edge.\nThe table normalizes native horizontal scrolling internally, so column requests retain the same\nvisible indices in either direction. Left/Right cell focus, keyboard/pointer resizing, keyboard\nreordering, drop markers and the column menu follow the physical direction. Changing direction\nat runtime preserves the logical inline-start offset. Scala exposes the same contract through\n`directionProperty` and `TableDirection.LeftToRight` / `RightToLeft`.\n\nPaged or scrolling content headers can declare their reserved height with `headerRows`. For\n`dataGrid`, one row is one card height and the header always spans the full responsive grid width;\nfor `tableView` and `virtualList`, rows use the table row height and estimated item height\nrespectively. If `headerRows` is omitted, the browser continues to measure the rendered header.\n\n## SSR and non-JavaScript behavior\n\nSSR renders a stable paged or crawl slice. After successful hydration, `tableView`, `dataGrid`, and `virtualList` automatically switch the default fallback to scrolling and retain the server-rendered offset. Set `paging: true` to keep paging in the browser. Visitors get no control to switch the mode: the pager footer exists exactly while the collection pages, so the server renders it for crawlers and the browser removes it after switching to scrolling. A crawler cannot scroll; use crawlability or paging when deeper collection content must be addressable without JavaScript.\n\n## API overview\n\n- `tab`, `tabs`, `carousel`\n- `column`, `columnGroup`, `valueColumn`, `ColumnGroupOptions`, `ValueColumnOptions`, `tableView`, `dataGrid`, `virtualList`\n- `remoteSource`, `RemoteSource`, `RemotePage`, `SortSpec`\n- `TabsOptions`, `CarouselOptions`, `TableViewOptions<T>`, `TableViewHandle<T>`, `TablePosition`, `TableRowContext<T>`, `DataGridOptions`, `VirtualListOptions`\n\n## Related modules\n\n- [`@anjunar/scalajs-ui-core`](../scalajs-ui-core/README.md) provides state and render callbacks.\n- [`@anjunar/scalajs-ui-viewport`](../scalajs-ui-viewport/README.md) provides the global UI layer.\n","readmeFilename":"README.md"}