decantr 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (382) hide show
  1. package/AGENTS.md +868 -0
  2. package/CHANGELOG.md +255 -0
  3. package/CLAUDE.md +178 -0
  4. package/LICENSE +21 -0
  5. package/README.md +229 -0
  6. package/cli/art.js +127 -0
  7. package/cli/commands/a11y.js +61 -0
  8. package/cli/commands/audit.js +225 -0
  9. package/cli/commands/build.js +38 -0
  10. package/cli/commands/dev.js +18 -0
  11. package/cli/commands/doctor.js +197 -0
  12. package/cli/commands/figma-sync.js +48 -0
  13. package/cli/commands/figma-tokens.js +55 -0
  14. package/cli/commands/generate.js +26 -0
  15. package/cli/commands/init.js +116 -0
  16. package/cli/commands/lint.js +209 -0
  17. package/cli/commands/mcp.js +530 -0
  18. package/cli/commands/migrate.js +175 -0
  19. package/cli/commands/test.js +38 -0
  20. package/cli/commands/validate.js +354 -0
  21. package/cli/index.js +113 -0
  22. package/package.json +95 -0
  23. package/reference/atoms.md +517 -0
  24. package/reference/behaviors.md +384 -0
  25. package/reference/build-tooling.md +275 -0
  26. package/reference/color-guidelines.md +965 -0
  27. package/reference/component-lifecycle.md +137 -0
  28. package/reference/compound-spacing.md +95 -0
  29. package/reference/decantation-process.md +499 -0
  30. package/reference/dev-server-routes.md +93 -0
  31. package/reference/form-system.md +253 -0
  32. package/reference/i18n.md +336 -0
  33. package/reference/icons.md +576 -0
  34. package/reference/llm-primer.md +953 -0
  35. package/reference/plugins.md +252 -0
  36. package/reference/registry-consumption.md +76 -0
  37. package/reference/router.md +217 -0
  38. package/reference/shells.md +116 -0
  39. package/reference/spatial-guidelines.md +541 -0
  40. package/reference/ssr.md +234 -0
  41. package/reference/state-data.md +215 -0
  42. package/reference/state-patterns.md +166 -0
  43. package/reference/state.md +194 -0
  44. package/reference/style-system.md +110 -0
  45. package/reference/tokens.md +460 -0
  46. package/src/app.js +19 -0
  47. package/src/chart/_animate.js +266 -0
  48. package/src/chart/_base.js +109 -0
  49. package/src/chart/_data.js +209 -0
  50. package/src/chart/_format.js +106 -0
  51. package/src/chart/_interact.js +364 -0
  52. package/src/chart/_palette.js +105 -0
  53. package/src/chart/_renderer.js +52 -0
  54. package/src/chart/_scene.js +262 -0
  55. package/src/chart/_shared.js +371 -0
  56. package/src/chart/index.js +637 -0
  57. package/src/chart/layouts/_layout-base.js +328 -0
  58. package/src/chart/layouts/cartesian.js +148 -0
  59. package/src/chart/layouts/hierarchy.js +562 -0
  60. package/src/chart/layouts/polar.js +101 -0
  61. package/src/chart/renderers/canvas.js +179 -0
  62. package/src/chart/renderers/svg.js +256 -0
  63. package/src/chart/renderers/webgpu.js +715 -0
  64. package/src/chart/types/_type-base.js +26 -0
  65. package/src/chart/types/area.js +134 -0
  66. package/src/chart/types/bar.js +173 -0
  67. package/src/chart/types/box-plot.js +125 -0
  68. package/src/chart/types/bubble.js +63 -0
  69. package/src/chart/types/candlestick.js +115 -0
  70. package/src/chart/types/chord.js +85 -0
  71. package/src/chart/types/combination.js +108 -0
  72. package/src/chart/types/funnel.js +68 -0
  73. package/src/chart/types/gauge.js +163 -0
  74. package/src/chart/types/heatmap.js +98 -0
  75. package/src/chart/types/histogram.js +71 -0
  76. package/src/chart/types/line.js +111 -0
  77. package/src/chart/types/org-chart.js +93 -0
  78. package/src/chart/types/pie.js +81 -0
  79. package/src/chart/types/radar.js +96 -0
  80. package/src/chart/types/radial.js +68 -0
  81. package/src/chart/types/range-area.js +55 -0
  82. package/src/chart/types/range-bar.js +61 -0
  83. package/src/chart/types/sankey.js +73 -0
  84. package/src/chart/types/scatter.js +66 -0
  85. package/src/chart/types/sparkline.js +81 -0
  86. package/src/chart/types/sunburst.js +69 -0
  87. package/src/chart/types/swimlane.js +88 -0
  88. package/src/chart/types/treemap.js +62 -0
  89. package/src/chart/types/waterfall.js +100 -0
  90. package/src/components/_base.js +1658 -0
  91. package/src/components/_behaviors.js +1140 -0
  92. package/src/components/_primitives.js +534 -0
  93. package/src/components/_qr-encoder.js +539 -0
  94. package/src/components/accordion.js +207 -0
  95. package/src/components/affix.js +62 -0
  96. package/src/components/alert-dialog.js +75 -0
  97. package/src/components/alert.js +47 -0
  98. package/src/components/aspect-ratio.js +24 -0
  99. package/src/components/avatar-group.js +55 -0
  100. package/src/components/avatar.js +38 -0
  101. package/src/components/back-top.js +75 -0
  102. package/src/components/badge.js +74 -0
  103. package/src/components/banner.js +68 -0
  104. package/src/components/breadcrumb.js +162 -0
  105. package/src/components/button.js +115 -0
  106. package/src/components/calendar.js +131 -0
  107. package/src/components/card.js +192 -0
  108. package/src/components/carousel.js +98 -0
  109. package/src/components/cascader.js +261 -0
  110. package/src/components/checkbox.js +80 -0
  111. package/src/components/chip.js +81 -0
  112. package/src/components/code-block.js +82 -0
  113. package/src/components/collapsible.js +50 -0
  114. package/src/components/color-palette.js +438 -0
  115. package/src/components/color-picker.js +314 -0
  116. package/src/components/combobox.js +181 -0
  117. package/src/components/command.js +174 -0
  118. package/src/components/comment.js +206 -0
  119. package/src/components/context-menu.js +76 -0
  120. package/src/components/data-table.js +724 -0
  121. package/src/components/date-picker.js +217 -0
  122. package/src/components/date-range-picker.js +244 -0
  123. package/src/components/datetime-picker.js +271 -0
  124. package/src/components/descriptions.js +68 -0
  125. package/src/components/drawer.js +179 -0
  126. package/src/components/dropdown.js +88 -0
  127. package/src/components/empty.js +41 -0
  128. package/src/components/float-button.js +90 -0
  129. package/src/components/form.js +106 -0
  130. package/src/components/hover-card.js +49 -0
  131. package/src/components/icon.js +87 -0
  132. package/src/components/image.js +97 -0
  133. package/src/components/index.js +117 -0
  134. package/src/components/input-group.js +75 -0
  135. package/src/components/input-number.js +155 -0
  136. package/src/components/input-otp.js +178 -0
  137. package/src/components/input.js +91 -0
  138. package/src/components/kbd.js +36 -0
  139. package/src/components/label.js +25 -0
  140. package/src/components/list.js +118 -0
  141. package/src/components/masked-input.js +236 -0
  142. package/src/components/mentions.js +165 -0
  143. package/src/components/menu.js +259 -0
  144. package/src/components/message.js +80 -0
  145. package/src/components/modal.js +147 -0
  146. package/src/components/navigation-menu.js +166 -0
  147. package/src/components/notification.js +84 -0
  148. package/src/components/pagination.js +104 -0
  149. package/src/components/placeholder.js +132 -0
  150. package/src/components/popconfirm.js +70 -0
  151. package/src/components/popover.js +58 -0
  152. package/src/components/progress.js +61 -0
  153. package/src/components/qrcode.js +251 -0
  154. package/src/components/radiogroup.js +120 -0
  155. package/src/components/range-slider.js +176 -0
  156. package/src/components/rate.js +186 -0
  157. package/src/components/resizable.js +83 -0
  158. package/src/components/result.js +57 -0
  159. package/src/components/scroll-area.js +43 -0
  160. package/src/components/segmented.js +97 -0
  161. package/src/components/select.js +165 -0
  162. package/src/components/separator.js +31 -0
  163. package/src/components/shell.js +407 -0
  164. package/src/components/skeleton.js +39 -0
  165. package/src/components/slider.js +141 -0
  166. package/src/components/sortable-list.js +176 -0
  167. package/src/components/space.js +42 -0
  168. package/src/components/spinner.js +112 -0
  169. package/src/components/splitter.js +147 -0
  170. package/src/components/statistic.js +136 -0
  171. package/src/components/steps.js +99 -0
  172. package/src/components/switch.js +95 -0
  173. package/src/components/table.js +44 -0
  174. package/src/components/tabs.js +216 -0
  175. package/src/components/tag.js +115 -0
  176. package/src/components/textarea.js +82 -0
  177. package/src/components/time-picker.js +153 -0
  178. package/src/components/time-range-picker.js +170 -0
  179. package/src/components/timeline.js +226 -0
  180. package/src/components/toast.js +71 -0
  181. package/src/components/toggle.js +213 -0
  182. package/src/components/tooltip.js +57 -0
  183. package/src/components/tour.js +159 -0
  184. package/src/components/transfer.js +163 -0
  185. package/src/components/tree-select.js +274 -0
  186. package/src/components/tree.js +141 -0
  187. package/src/components/typography.js +136 -0
  188. package/src/components/upload.js +118 -0
  189. package/src/components/visually-hidden.js +20 -0
  190. package/src/components/watermark.js +124 -0
  191. package/src/core/index.js +539 -0
  192. package/src/core/lifecycle.js +69 -0
  193. package/src/css/atoms.js +651 -0
  194. package/src/css/components.js +940 -0
  195. package/src/css/derive.js +1296 -0
  196. package/src/css/index.js +265 -0
  197. package/src/css/runtime.js +268 -0
  198. package/src/css/styles/addons/bioluminescent.js +93 -0
  199. package/src/css/styles/addons/clay.js +70 -0
  200. package/src/css/styles/addons/clean.js +57 -0
  201. package/src/css/styles/addons/command-center.js +143 -0
  202. package/src/css/styles/addons/dopamine.js +83 -0
  203. package/src/css/styles/addons/editorial.js +80 -0
  204. package/src/css/styles/addons/glassmorphism.js +99 -0
  205. package/src/css/styles/addons/liquid-glass.js +105 -0
  206. package/src/css/styles/addons/prismatic.js +100 -0
  207. package/src/css/styles/addons/retro.js +63 -0
  208. package/src/css/styles/auradecantism.js +96 -0
  209. package/src/css/theme-registry.js +444 -0
  210. package/src/data/entity.js +281 -0
  211. package/src/data/index.js +13 -0
  212. package/src/data/persist.js +225 -0
  213. package/src/data/query.js +839 -0
  214. package/src/data/realtime.js +299 -0
  215. package/src/data/url.js +177 -0
  216. package/src/data/worker.js +134 -0
  217. package/src/explorer/archetypes.js +243 -0
  218. package/src/explorer/atoms.js +228 -0
  219. package/src/explorer/charts.js +497 -0
  220. package/src/explorer/components.js +129 -0
  221. package/src/explorer/foundations.js +949 -0
  222. package/src/explorer/icons.js +178 -0
  223. package/src/explorer/patterns.js +247 -0
  224. package/src/explorer/recipes.js +194 -0
  225. package/src/explorer/shared/pattern-examples.js +1337 -0
  226. package/src/explorer/shared/showcase-renderer.js +958 -0
  227. package/src/explorer/shared/spec-table.js +41 -0
  228. package/src/explorer/shared/usage-links.js +87 -0
  229. package/src/explorer/shell-config.js +10 -0
  230. package/src/explorer/shells.js +551 -0
  231. package/src/explorer/styles.js +161 -0
  232. package/src/explorer/tokens.js +262 -0
  233. package/src/explorer/tools.js +525 -0
  234. package/src/form/index.js +804 -0
  235. package/src/i18n/index.js +251 -0
  236. package/src/icons/essential.js +479 -0
  237. package/src/icons/index.js +53 -0
  238. package/src/plugins/index.js +282 -0
  239. package/src/registry/archetypes/content-site.json +71 -0
  240. package/src/registry/archetypes/docs-explorer.json +23 -0
  241. package/src/registry/archetypes/ecommerce.json +104 -0
  242. package/src/registry/archetypes/financial-dashboard.json +77 -0
  243. package/src/registry/archetypes/index.json +41 -0
  244. package/src/registry/archetypes/portfolio.json +82 -0
  245. package/src/registry/archetypes/recipe-community.json +159 -0
  246. package/src/registry/archetypes/saas-dashboard.json +86 -0
  247. package/src/registry/architect/cross-cutting.json +45 -0
  248. package/src/registry/architect/domains/ecommerce.json +294 -0
  249. package/src/registry/architect/domains/financial-services.json +302 -0
  250. package/src/registry/architect/index.json +26 -0
  251. package/src/registry/architect/traits.json +379 -0
  252. package/src/registry/atoms.json +16 -0
  253. package/src/registry/chart-showcase.json +160 -0
  254. package/src/registry/chart.json +136 -0
  255. package/src/registry/components.json +8616 -0
  256. package/src/registry/core.json +216 -0
  257. package/src/registry/css.json +319 -0
  258. package/src/registry/data.json +135 -0
  259. package/src/registry/foundations.json +11 -0
  260. package/src/registry/icons.json +463 -0
  261. package/src/registry/index.json +101 -0
  262. package/src/registry/patterns/activity-feed.json +37 -0
  263. package/src/registry/patterns/article-content.json +27 -0
  264. package/src/registry/patterns/auth-form.json +37 -0
  265. package/src/registry/patterns/author-card.json +20 -0
  266. package/src/registry/patterns/card-grid.json +127 -0
  267. package/src/registry/patterns/category-nav.json +26 -0
  268. package/src/registry/patterns/chart-grid.json +36 -0
  269. package/src/registry/patterns/chat-interface.json +37 -0
  270. package/src/registry/patterns/checklist-card.json +55 -0
  271. package/src/registry/patterns/comparison-panel.json +27 -0
  272. package/src/registry/patterns/component-showcase.json +24 -0
  273. package/src/registry/patterns/contact-form.json +31 -0
  274. package/src/registry/patterns/cta-section.json +20 -0
  275. package/src/registry/patterns/data-table.json +37 -0
  276. package/src/registry/patterns/detail-header.json +83 -0
  277. package/src/registry/patterns/detail-panel.json +27 -0
  278. package/src/registry/patterns/explorer-shell.json +22 -0
  279. package/src/registry/patterns/filter-bar.json +33 -0
  280. package/src/registry/patterns/filter-sidebar.json +27 -0
  281. package/src/registry/patterns/form-sections.json +110 -0
  282. package/src/registry/patterns/goal-tracker.json +27 -0
  283. package/src/registry/patterns/hero.json +107 -0
  284. package/src/registry/patterns/index.json +47 -0
  285. package/src/registry/patterns/kpi-grid.json +36 -0
  286. package/src/registry/patterns/media-gallery.json +20 -0
  287. package/src/registry/patterns/order-history.json +20 -0
  288. package/src/registry/patterns/pagination.json +19 -0
  289. package/src/registry/patterns/photo-to-recipe.json +36 -0
  290. package/src/registry/patterns/pipeline-tracker.json +28 -0
  291. package/src/registry/patterns/post-list.json +27 -0
  292. package/src/registry/patterns/pricing-table.json +32 -0
  293. package/src/registry/patterns/scorecard.json +28 -0
  294. package/src/registry/patterns/search-bar.json +20 -0
  295. package/src/registry/patterns/specimen-grid.json +19 -0
  296. package/src/registry/patterns/stat-card.json +55 -0
  297. package/src/registry/patterns/stats-bar.json +55 -0
  298. package/src/registry/patterns/steps-card.json +55 -0
  299. package/src/registry/patterns/table-of-contents.json +19 -0
  300. package/src/registry/patterns/testimonials.json +21 -0
  301. package/src/registry/patterns/timeline.json +27 -0
  302. package/src/registry/patterns/token-inspector.json +21 -0
  303. package/src/registry/patterns/wizard.json +27 -0
  304. package/src/registry/recipe-auradecantism.json +69 -0
  305. package/src/registry/recipe-clean.json +65 -0
  306. package/src/registry/recipe-command-center.json +78 -0
  307. package/src/registry/router.json +73 -0
  308. package/src/registry/schema/README.md +197 -0
  309. package/src/registry/skeletons.json +259 -0
  310. package/src/registry/state.json +137 -0
  311. package/src/registry/tokens.json +40 -0
  312. package/src/router/hash.js +17 -0
  313. package/src/router/history.js +18 -0
  314. package/src/router/index.js +598 -0
  315. package/src/ssr/index.js +922 -0
  316. package/src/state/arrays.js +181 -0
  317. package/src/state/devtools.js +647 -0
  318. package/src/state/index.js +498 -0
  319. package/src/state/middleware.js +288 -0
  320. package/src/state/scheduler.js +206 -0
  321. package/src/state/store.js +300 -0
  322. package/src/tags/index.js +19 -0
  323. package/src/tannins/auth.js +396 -0
  324. package/src/test/dom.js +352 -0
  325. package/src/test/index.js +62 -0
  326. package/src/test/state.js +306 -0
  327. package/tools/a11y-audit.js +487 -0
  328. package/tools/analyzer.js +315 -0
  329. package/tools/audit.js +706 -0
  330. package/tools/builder.js +1422 -0
  331. package/tools/css-extract.js +188 -0
  332. package/tools/dev-server.js +316 -0
  333. package/tools/dts-gen.js +1260 -0
  334. package/tools/figma-components.js +329 -0
  335. package/tools/figma-patterns.js +516 -0
  336. package/tools/figma-plugin/code.js +453 -0
  337. package/tools/figma-plugin/manifest.json +14 -0
  338. package/tools/figma-plugin/ui.html +268 -0
  339. package/tools/figma-render.js +293 -0
  340. package/tools/figma-tokens.js +712 -0
  341. package/tools/figma-upload.js +318 -0
  342. package/tools/generate.js +738 -0
  343. package/tools/icons.js +133 -0
  344. package/tools/init-templates.js +265 -0
  345. package/tools/install-hooks.sh +5 -0
  346. package/tools/migrations/0.5.0.js +53 -0
  347. package/tools/migrations/0.6.0.js +95 -0
  348. package/tools/minify.js +170 -0
  349. package/tools/pre-commit +4 -0
  350. package/tools/registry.js +662 -0
  351. package/tools/reset-playground.js +61 -0
  352. package/tools/starter-templates/content-site/app.js +49 -0
  353. package/tools/starter-templates/content-site/essence.js +19 -0
  354. package/tools/starter-templates/content-site/pages.js +31 -0
  355. package/tools/starter-templates/ecommerce/app.js +50 -0
  356. package/tools/starter-templates/ecommerce/essence.js +19 -0
  357. package/tools/starter-templates/ecommerce/pages.js +31 -0
  358. package/tools/starter-templates/landing-page/app.js +38 -0
  359. package/tools/starter-templates/landing-page/essence.js +18 -0
  360. package/tools/starter-templates/landing-page/pages.js +21 -0
  361. package/tools/starter-templates/portfolio/app.js +45 -0
  362. package/tools/starter-templates/portfolio/essence.js +19 -0
  363. package/tools/starter-templates/portfolio/pages.js +33 -0
  364. package/tools/starter-templates/saas-dashboard/app.js +70 -0
  365. package/tools/starter-templates/saas-dashboard/essence.js +19 -0
  366. package/tools/starter-templates/saas-dashboard/pages.js +31 -0
  367. package/tools/verify-pack.js +203 -0
  368. package/types/chart.d.ts +77 -0
  369. package/types/components.d.ts +587 -0
  370. package/types/core.d.ts +89 -0
  371. package/types/css.d.ts +149 -0
  372. package/types/data.d.ts +238 -0
  373. package/types/form.d.ts +164 -0
  374. package/types/i18n.d.ts +51 -0
  375. package/types/icons.d.ts +27 -0
  376. package/types/index.d.ts +13 -0
  377. package/types/router.d.ts +116 -0
  378. package/types/ssr.d.ts +102 -0
  379. package/types/state.d.ts +83 -0
  380. package/types/tags.d.ts +62 -0
  381. package/types/tannins.d.ts +63 -0
  382. package/types/test.d.ts +48 -0
@@ -0,0 +1,724 @@
1
+ import { h, text, onDestroy } from '../core/index.js';
2
+ import { createSignal, createEffect, batch } from '../state/index.js';
3
+ import { tags } from '../tags/index.js';
4
+ import { injectBase, cx } from './_base.js';
5
+ import { caret, createCheckControl } from './_behaviors.js';
6
+
7
+ const { div, button: buttonTag, span, label: labelTag, select: selectTag, option: optionTag } = tags;
8
+
9
+ // ═══════════════════════════════════════════════════════════════
10
+ // HELPERS
11
+ // ═══════════════════════════════════════════════════════════════
12
+
13
+ const resolve = (v) => typeof v === 'function' ? v() : v;
14
+ const ROW_H = 40;
15
+ const VIRT_THRESHOLD = 500;
16
+ const MIN_COL_W = 50;
17
+
18
+ function defaultCmp(a, b) {
19
+ if (a == null && b == null) return 0;
20
+ if (a == null) return -1;
21
+ if (b == null) return 1;
22
+ if (typeof a === 'number' && typeof b === 'number') return a - b;
23
+ return String(a).localeCompare(String(b));
24
+ }
25
+
26
+ function csvCell(v) {
27
+ if (v == null) return '';
28
+ const s = String(v);
29
+ return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
30
+ }
31
+
32
+ // ═══════════════════════════════════════════════════════════════
33
+ // DATATABLE COMPONENT
34
+ // ═══════════════════════════════════════════════════════════════
35
+
36
+ /**
37
+ * DataTable — Enterprise data grid with sorting, pagination, selection,
38
+ * column pinning, cell editing, row expansion, filtering, export, virtual
39
+ * scrolling, and column resizing.
40
+ *
41
+ * @param {Object} props
42
+ * @param {Array<{key:string,label:string,width?:string,sortable?:boolean,filterable?:boolean,pinned?:'left'|'right',render?:Function,editable?:boolean,align?:'left'|'center'|'right',sort?:Function}>} props.columns
43
+ * @param {Array<Object>|Function} props.data
44
+ * @param {Object} [props.pagination]
45
+ * @param {'single'|'multi'|'none'} [props.selection='none']
46
+ * @param {Function} [props.onSelectionChange]
47
+ * @param {boolean} [props.striped=false]
48
+ * @param {boolean} [props.hoverable=true]
49
+ * @param {boolean} [props.stickyHeader=false]
50
+ * @param {Function} [props.onSort]
51
+ * @param {Function} [props.rowKey]
52
+ * @param {Function} [props.onCellEdit]
53
+ * @param {boolean} [props.expandable=false]
54
+ * @param {Function} [props.expandRender]
55
+ * @param {boolean} [props.exportable=false]
56
+ * @param {string} [props.emptyText='No data']
57
+ * @param {string} [props.class]
58
+ * @returns {HTMLElement}
59
+ */
60
+ export function DataTable(props = {}) {
61
+ injectBase();
62
+
63
+ const {
64
+ columns: rawCols = [],
65
+ data: rawData,
66
+ pagination: pgCfg,
67
+ selection = 'none',
68
+ onSelectionChange,
69
+ striped = false,
70
+ hoverable = true,
71
+ stickyHeader = false,
72
+ onSort,
73
+ rowKey = (r, i) => i,
74
+ onCellEdit,
75
+ expandable = false,
76
+ expandRender,
77
+ exportable = false,
78
+ emptyText = 'No data',
79
+ class: cls
80
+ } = props;
81
+
82
+ // ─────────────────────────────────────────────────────────────
83
+ // STATE
84
+ // ─────────────────────────────────────────────────────────────
85
+
86
+ const [sortCols, setSortCols] = createSignal([]);
87
+ const [page, setPage] = createSignal(1);
88
+ const [pageSize, setPageSize] = createSignal(pgCfg ? pgCfg.pageSize || 10 : 10);
89
+ const [selected, setSelected] = createSignal(new Set());
90
+ let lastClickIdx = -1;
91
+ const [expanded, setExpanded] = createSignal(new Set());
92
+ const [filters, setFilters] = createSignal({});
93
+ const [colWidths, setColWidths] = createSignal({});
94
+ const [scrollTop, setScrollTop] = createSignal(0);
95
+
96
+ // ─────────────────────────────────────────────────────────────
97
+ // DERIVED DATA PIPELINE
98
+ // ─────────────────────────────────────────────────────────────
99
+
100
+ const getData = () => resolve(rawData) || [];
101
+
102
+ const getFiltered = () => {
103
+ const d = getData();
104
+ const f = filters();
105
+ const keys = Object.keys(f).filter(k => f[k]);
106
+ if (!keys.length) return d;
107
+ return d.filter(row =>
108
+ keys.every(k => {
109
+ const v = row[k];
110
+ return v != null && String(v).toLowerCase().includes(f[k].toLowerCase());
111
+ })
112
+ );
113
+ };
114
+
115
+ const getSorted = () => {
116
+ const d = getFiltered();
117
+ const sc = sortCols();
118
+ if (!sc.length) return d;
119
+ const sorted = [...d];
120
+ sorted.sort((a, b) => {
121
+ for (const { key, direction } of sc) {
122
+ const col = rawCols.find(c => c.key === key);
123
+ const cmp = col && col.sort ? col.sort : defaultCmp;
124
+ const r = cmp(a[key], b[key]);
125
+ if (r !== 0) return direction === 'asc' ? r : -r;
126
+ }
127
+ return 0;
128
+ });
129
+ return sorted;
130
+ };
131
+
132
+ const getTotal = () => {
133
+ if (pgCfg && pgCfg.serverSide && pgCfg.total != null) return resolve(pgCfg.total);
134
+ return getSorted().length;
135
+ };
136
+
137
+ const getPageCount = () => Math.max(1, Math.ceil(getTotal() / pageSize()));
138
+
139
+ const getPageData = () => {
140
+ const sorted = getSorted();
141
+ if (!pgCfg || pgCfg.serverSide) return sorted;
142
+ const ps = pageSize();
143
+ const p = page();
144
+ return sorted.slice((p - 1) * ps, p * ps);
145
+ };
146
+
147
+ // ─────────────────────────────────────────────────────────────
148
+ // SORT LOGIC
149
+ // ─────────────────────────────────────────────────────────────
150
+
151
+ function handleSort(key, multi) {
152
+ setSortCols(prev => {
153
+ const idx = prev.findIndex(s => s.key === key);
154
+ let next;
155
+ if (idx === -1) {
156
+ const entry = { key, direction: 'asc' };
157
+ next = multi ? [...prev, entry] : [entry];
158
+ } else {
159
+ const cur = prev[idx];
160
+ if (cur.direction === 'asc') {
161
+ next = [...prev];
162
+ next[idx] = { key, direction: 'desc' };
163
+ } else {
164
+ next = prev.filter((_, i) => i !== idx);
165
+ }
166
+ if (!multi && next.length > 1) next = next.filter(s => s.key === key);
167
+ }
168
+ if (onSort && next.length) onSort(next[next.length - 1]);
169
+ return next;
170
+ });
171
+ setPage(1);
172
+ }
173
+
174
+ // ─────────────────────────────────────────────────────────────
175
+ // SELECTION LOGIC
176
+ // ─────────────────────────────────────────────────────────────
177
+
178
+ function fireSelection(set) {
179
+ if (!onSelectionChange) return;
180
+ const data = getPageData();
181
+ onSelectionChange(data.filter((r, i) => set.has(rowKey(r, i))));
182
+ }
183
+
184
+ function toggleSelect(row, idx, shiftKey) {
185
+ if (selection === 'none') return;
186
+ const key = rowKey(row, idx);
187
+
188
+ if (selection === 'single') {
189
+ setSelected(prev => {
190
+ const next = new Set();
191
+ if (!prev.has(key)) next.add(key);
192
+ fireSelection(next);
193
+ return next;
194
+ });
195
+ } else {
196
+ if (shiftKey && lastClickIdx >= 0) {
197
+ const rows = getPageData();
198
+ const lo = Math.min(lastClickIdx, idx);
199
+ const hi = Math.max(lastClickIdx, idx);
200
+ setSelected(prev => {
201
+ const next = new Set(prev);
202
+ for (let i = lo; i <= hi; i++) next.add(rowKey(rows[i], i));
203
+ fireSelection(next);
204
+ return next;
205
+ });
206
+ } else {
207
+ setSelected(prev => {
208
+ const next = new Set(prev);
209
+ next.has(key) ? next.delete(key) : next.add(key);
210
+ fireSelection(next);
211
+ return next;
212
+ });
213
+ }
214
+ lastClickIdx = idx;
215
+ }
216
+ }
217
+
218
+ function toggleSelectAll() {
219
+ const rows = getPageData();
220
+ const sel = selected();
221
+ const allKeys = rows.map((r, i) => rowKey(r, i));
222
+ const allSelected = allKeys.length > 0 && allKeys.every(k => sel.has(k));
223
+ const next = new Set(sel);
224
+ if (allSelected) allKeys.forEach(k => next.delete(k));
225
+ else allKeys.forEach(k => next.add(k));
226
+ setSelected(next);
227
+ fireSelection(next);
228
+ }
229
+
230
+ // ─────────────────────────────────────────────────────────────
231
+ // EXPAND / FILTER / EXPORT
232
+ // ─────────────────────────────────────────────────────────────
233
+
234
+ function toggleExpand(row, idx) {
235
+ const key = rowKey(row, idx);
236
+ setExpanded(prev => {
237
+ const next = new Set(prev);
238
+ next.has(key) ? next.delete(key) : next.add(key);
239
+ return next;
240
+ });
241
+ }
242
+
243
+ function setFilter(key, value) {
244
+ setFilters(prev => ({ ...prev, [key]: value }));
245
+ setPage(1);
246
+ }
247
+
248
+ function exportCSV() {
249
+ const rows = getSorted();
250
+ const header = rawCols.map(c => csvCell(c.label)).join(',');
251
+ const body = rows.map(r => rawCols.map(c => csvCell(r[c.key])).join(',')).join('\n');
252
+ const csv = header + '\n' + body;
253
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
254
+ const url = URL.createObjectURL(blob);
255
+ const a = document.createElement('a');
256
+ a.href = url;
257
+ a.download = 'data.csv';
258
+ a.click();
259
+ URL.revokeObjectURL(url);
260
+ }
261
+
262
+ // ─────────────────────────────────────────────────────────────
263
+ // CELL EDITING
264
+ // ─────────────────────────────────────────────────────────────
265
+
266
+ function startEdit(td, row, col) {
267
+ if (td.querySelector('input')) return;
268
+ const oldValue = row[col.key];
269
+ td.classList.add('d-datatable-td-editing');
270
+ const input = h('input', {
271
+ type: 'text',
272
+ value: oldValue != null ? String(oldValue) : '',
273
+ class: 'd-datatable-edit-input',
274
+ onKeydown(e) {
275
+ if (e.key === 'Enter') commitEdit();
276
+ if (e.key === 'Escape') cancelEdit();
277
+ },
278
+ onBlur() { commitEdit(); }
279
+ });
280
+
281
+ td.textContent = '';
282
+ td.appendChild(input);
283
+ input.focus();
284
+ input.select();
285
+
286
+ function commitEdit() {
287
+ const newValue = input.value;
288
+ td.classList.remove('d-datatable-td-editing');
289
+ td.textContent = newValue;
290
+ if (newValue !== String(oldValue ?? '') && onCellEdit) {
291
+ onCellEdit({ row, column: col, value: newValue, oldValue });
292
+ }
293
+ }
294
+
295
+ function cancelEdit() {
296
+ td.classList.remove('d-datatable-td-editing');
297
+ td.textContent = oldValue != null ? String(oldValue) : '';
298
+ }
299
+ }
300
+
301
+ // ─────────────────────────────────────────────────────────────
302
+ // COLUMN RESIZE
303
+ // ─────────────────────────────────────────────────────────────
304
+
305
+ let _resizeCleanup = null;
306
+
307
+ function initResize(e, col, thEl) {
308
+ e.preventDefault();
309
+ const startX = e.clientX;
310
+ const startW = thEl.offsetWidth;
311
+
312
+ function onMove(ev) {
313
+ const diff = ev.clientX - startX;
314
+ const newW = Math.max(MIN_COL_W, startW + diff);
315
+ setColWidths(prev => ({ ...prev, [col.key]: newW }));
316
+ thEl.style.width = newW + 'px';
317
+ }
318
+
319
+ function onUp() {
320
+ document.removeEventListener('mousemove', onMove);
321
+ document.removeEventListener('mouseup', onUp);
322
+ _resizeCleanup = null;
323
+ }
324
+
325
+ document.addEventListener('mousemove', onMove);
326
+ document.addEventListener('mouseup', onUp);
327
+ _resizeCleanup = onUp;
328
+ }
329
+
330
+ // ─────────────────────────────────────────────────────────────
331
+ // PIN OFFSETS
332
+ // ─────────────────────────────────────────────────────────────
333
+
334
+ function getEffectiveCols() { return rawCols; }
335
+
336
+ // ─────────────────────────────────────────────────────────────
337
+ // BUILD DOM
338
+ // ─────────────────────────────────────────────────────────────
339
+
340
+ const root = div({
341
+ class: cx('d-datatable', cls),
342
+ role: 'region',
343
+ 'aria-label': 'Data table'
344
+ });
345
+
346
+ // Toolbar
347
+ if (exportable) {
348
+ root.appendChild(div({ class: 'd-datatable-header' },
349
+ buttonTag({
350
+ class: 'd-datatable-export-btn',
351
+ type: 'button',
352
+ onclick: exportCSV,
353
+ 'aria-label': 'Export as CSV'
354
+ }, 'Export CSV')
355
+ ));
356
+ }
357
+
358
+ // Scroll container
359
+ const scrollWrap = div({ class: 'd-datatable-scroll' });
360
+ scrollWrap.style.overflow = 'auto';
361
+ scrollWrap.style.position = 'relative';
362
+
363
+ // Table element
364
+ const table = h('table', {
365
+ class: cx('d-datatable-table', striped && 'd-datatable-striped', hoverable && 'd-datatable-hoverable'),
366
+ role: 'grid'
367
+ });
368
+
369
+ // thead
370
+ const thead = h('thead', { class: stickyHeader ? 'd-datatable-sticky' : null });
371
+ const headerRow = h('tr');
372
+
373
+ // Expand spacer column
374
+ if (expandable) {
375
+ headerRow.appendChild(h('th', {
376
+ class: 'd-datatable-th',
377
+ 'aria-label': 'Expand'
378
+ }));
379
+ headerRow.lastChild.style.width = '40px';
380
+ }
381
+
382
+ // Selection column
383
+ if (selection === 'multi') {
384
+ const selAllTh = h('th', { class: 'd-datatable-th' });
385
+ selAllTh.style.width = '40px';
386
+ const { wrap: selAllWrap, input: selAllCb } = createCheckControl({ 'aria-label': 'Select all rows' });
387
+ selAllCb.addEventListener('change', toggleSelectAll);
388
+ selAllTh.appendChild(selAllWrap);
389
+ headerRow.appendChild(selAllTh);
390
+
391
+ createEffect(() => {
392
+ const sel = selected();
393
+ const rows = getPageData();
394
+ const allKeys = rows.map((r, i) => rowKey(r, i));
395
+ const allSel = allKeys.length > 0 && allKeys.every(k => sel.has(k));
396
+ const someSel = !allSel && allKeys.some(k => sel.has(k));
397
+ selAllCb.checked = allSel;
398
+ selAllCb.indeterminate = someSel;
399
+ });
400
+ }
401
+
402
+ // Data columns
403
+ rawCols.forEach((col) => {
404
+ const th = h('th', {
405
+ class: cx(
406
+ 'd-datatable-th',
407
+ col.sortable && 'd-datatable-th-sortable',
408
+ col.pinned === 'left' && 'd-datatable-pinned-left',
409
+ col.pinned === 'right' && 'd-datatable-pinned-right'
410
+ ),
411
+ 'aria-sort': 'none'
412
+ });
413
+ th.style.width = col.width || 'auto';
414
+ th.style.textAlign = col.align || 'left';
415
+ if (col.pinned) { th.style.position = 'sticky'; th.style.zIndex = '3'; }
416
+
417
+ th.appendChild(span(col.label));
418
+
419
+ if (col.sortable) {
420
+ const sortIndicator = span({ class: 'd-datatable-sort-icon', 'aria-hidden': 'true' });
421
+ th.appendChild(sortIndicator);
422
+
423
+ createEffect(() => {
424
+ const sc = sortCols();
425
+ const entry = sc.find(s => s.key === col.key);
426
+ sortIndicator.replaceChildren(entry ? (entry.direction === 'asc' ? caret('up') : caret('down')) : caret('down'));
427
+ th.setAttribute('aria-sort', entry ? (entry.direction === 'asc' ? 'ascending' : 'descending') : 'none');
428
+ th.classList.toggle('d-datatable-th-sorted-asc', !!(entry && entry.direction === 'asc'));
429
+ th.classList.toggle('d-datatable-th-sorted-desc', !!(entry && entry.direction === 'desc'));
430
+ });
431
+
432
+ th.addEventListener('click', (e) => handleSort(col.key, e.shiftKey));
433
+ th.style.cursor = 'pointer';
434
+ th.style.userSelect = 'none';
435
+ }
436
+
437
+ if (col.filterable) {
438
+ const filterWrap = span({ class: 'd-datatable-filter-wrap' });
439
+ filterWrap.style.position = 'relative';
440
+ filterWrap.style.display = 'inline-block';
441
+
442
+ const filterBtn = buttonTag({
443
+ class: 'd-datatable-filter-icon',
444
+ type: 'button',
445
+ 'aria-label': `Filter ${col.label}`
446
+ }, '\u25BD');
447
+ filterBtn.addEventListener('click', (e) => {
448
+ e.stopPropagation();
449
+ const popup = filterWrap.querySelector('.d-datatable-filter-popup');
450
+ if (popup) {
451
+ popup.style.display = popup.style.display === 'none' ? 'block' : 'none';
452
+ if (popup.style.display === 'block') popup.querySelector('input').focus();
453
+ }
454
+ });
455
+
456
+ const filterPopup = div({ class: 'd-datatable-filter-popup' });
457
+ filterPopup.style.display = 'none';
458
+ filterPopup.addEventListener('click', (e) => e.stopPropagation());
459
+
460
+ const filterInput = h('input', {
461
+ type: 'text',
462
+ placeholder: `Filter ${col.label}...`,
463
+ 'aria-label': `Filter by ${col.label}`,
464
+ oninput(e) { setFilter(col.key, e.target.value); }
465
+ });
466
+ filterPopup.appendChild(filterInput);
467
+ filterWrap.appendChild(filterBtn);
468
+ filterWrap.appendChild(filterPopup);
469
+ th.appendChild(filterWrap);
470
+
471
+ createEffect(() => {
472
+ const f = filters();
473
+ filterBtn.classList.toggle('d-datatable-filter-active', !!f[col.key]);
474
+ });
475
+ }
476
+
477
+ const handle = span({ class: 'd-datatable-resize-handle', 'aria-hidden': 'true' });
478
+ handle.addEventListener('mousedown', (e) => initResize(e, col, th));
479
+ th.appendChild(handle);
480
+
481
+ headerRow.appendChild(th);
482
+ });
483
+
484
+ thead.appendChild(headerRow);
485
+ table.appendChild(thead);
486
+
487
+ // tbody (reactive)
488
+ const tbody = h('tbody');
489
+ table.appendChild(tbody);
490
+
491
+ let useVirtual = false;
492
+ const spacerTop = h('tr', { 'aria-hidden': 'true' });
493
+ spacerTop.style.height = '0px';
494
+ spacerTop.appendChild(h('td'));
495
+ spacerTop.firstChild.style.padding = '0';
496
+ spacerTop.firstChild.style.border = 'none';
497
+
498
+ const spacerBottom = h('tr', { 'aria-hidden': 'true' });
499
+ spacerBottom.style.height = '0px';
500
+ spacerBottom.appendChild(h('td'));
501
+ spacerBottom.firstChild.style.padding = '0';
502
+ spacerBottom.firstChild.style.border = 'none';
503
+
504
+ function buildRow(row, idx) {
505
+ const key = rowKey(row, idx);
506
+ const sel = selected();
507
+ const exp = expanded();
508
+ const isSelected = sel.has(key);
509
+ const isExpanded = exp.has(key);
510
+
511
+ const tr = h('tr', {
512
+ class: cx('d-datatable-row', isSelected && 'd-datatable-row-selected', isExpanded && 'd-datatable-row-expanded'),
513
+ 'data-row-key': key,
514
+ onclick(e) { toggleSelect(row, idx, e.shiftKey); }
515
+ });
516
+
517
+ if (expandable) {
518
+ const expandTd = h('td', { class: 'd-datatable-td' });
519
+ expandTd.style.width = '40px';
520
+ expandTd.style.textAlign = 'center';
521
+ const expandBtn = buttonTag({
522
+ type: 'button',
523
+ class: 'd-datatable-expand-btn',
524
+ 'aria-label': isExpanded ? 'Collapse row' : 'Expand row',
525
+ 'aria-expanded': isExpanded ? 'true' : 'false'
526
+ }, isExpanded ? caret('down') : caret('right'));
527
+ expandBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleExpand(row, idx); });
528
+ expandTd.appendChild(expandBtn);
529
+ tr.appendChild(expandTd);
530
+ }
531
+
532
+ if (selection === 'multi') {
533
+ const cbTd = h('td', { class: 'd-datatable-td' });
534
+ cbTd.style.width = '40px';
535
+ cbTd.style.textAlign = 'center';
536
+ const { wrap: cbWrap, input: rowCb } = createCheckControl({ 'aria-label': `Select row ${idx + 1}` });
537
+ rowCb.checked = isSelected;
538
+ rowCb.addEventListener('click', (e) => e.stopPropagation());
539
+ rowCb.addEventListener('change', () => toggleSelect(row, idx, false));
540
+ cbTd.appendChild(cbWrap);
541
+ tr.appendChild(cbTd);
542
+ }
543
+
544
+ rawCols.forEach(col => {
545
+ const val = row[col.key];
546
+ const content = col.render ? col.render(val, row) : (val != null ? String(val) : '');
547
+
548
+ const td = h('td', {
549
+ class: cx('d-datatable-td', col.pinned === 'left' && 'd-datatable-pinned-left', col.pinned === 'right' && 'd-datatable-pinned-right')
550
+ });
551
+ td.style.textAlign = col.align || 'left';
552
+ if (col.pinned) { td.style.position = 'sticky'; td.style.zIndex = '1'; }
553
+
554
+ if (typeof content === 'object' && content instanceof Node) td.appendChild(content);
555
+ else td.textContent = content;
556
+
557
+ if (col.editable) {
558
+ td.addEventListener('dblclick', (e) => { e.stopPropagation(); startEdit(td, row, col); });
559
+ td.style.cursor = 'text';
560
+ }
561
+
562
+ tr.appendChild(td);
563
+ });
564
+
565
+ const frag = [tr];
566
+
567
+ if (expandable && isExpanded && expandRender) {
568
+ const totalCols = rawCols.length + (selection === 'multi' ? 1 : 0) + 1;
569
+ const expandTr = h('tr', { class: 'd-datatable-expand-row' });
570
+ const expandTd = h('td', { colspan: totalCols, class: 'd-datatable-td' });
571
+ const content = expandRender(row);
572
+ if (content instanceof Node) expandTd.appendChild(content);
573
+ else expandTd.textContent = content;
574
+ expandTr.appendChild(expandTd);
575
+ frag.push(expandTr);
576
+ }
577
+
578
+ return frag;
579
+ }
580
+
581
+ createEffect(() => {
582
+ const rows = getPageData();
583
+ selected(); expanded(); filters(); sortCols(); page(); pageSize();
584
+
585
+ while (tbody.firstChild) tbody.removeChild(tbody.firstChild);
586
+
587
+ if (rows.length === 0) {
588
+ const totalCols = rawCols.length + (selection === 'multi' ? 1 : 0) + (expandable ? 1 : 0);
589
+ tbody.appendChild(h('tr', { class: 'd-datatable-empty' },
590
+ h('td', { colspan: totalCols, class: 'd-datatable-td' }, emptyText)
591
+ ));
592
+ return;
593
+ }
594
+
595
+ useVirtual = rows.length > VIRT_THRESHOLD;
596
+
597
+ if (useVirtual) {
598
+ const containerH = scrollWrap.clientHeight || 400;
599
+ const st = scrollTop();
600
+ const visibleStart = Math.max(0, Math.floor(st / ROW_H) - 5);
601
+ const visibleEnd = Math.min(rows.length, Math.ceil((st + containerH) / ROW_H) + 5);
602
+
603
+ spacerTop.style.height = (visibleStart * ROW_H) + 'px';
604
+ spacerBottom.style.height = ((rows.length - visibleEnd) * ROW_H) + 'px';
605
+
606
+ tbody.appendChild(spacerTop);
607
+ for (let i = visibleStart; i < visibleEnd; i++) {
608
+ buildRow(rows[i], i).forEach(tr => tbody.appendChild(tr));
609
+ }
610
+ tbody.appendChild(spacerBottom);
611
+ } else {
612
+ const frag = document.createDocumentFragment();
613
+ for (let i = 0; i < rows.length; i++) {
614
+ buildRow(rows[i], i).forEach(tr => frag.appendChild(tr));
615
+ }
616
+ tbody.appendChild(frag);
617
+ }
618
+ });
619
+
620
+ const onScroll = () => {
621
+ if (useVirtual) setScrollTop(scrollWrap.scrollTop);
622
+ };
623
+ scrollWrap.addEventListener('scroll', onScroll);
624
+
625
+ onDestroy(() => {
626
+ scrollWrap.removeEventListener('scroll', onScroll);
627
+ if (_resizeCleanup) _resizeCleanup();
628
+ });
629
+
630
+ scrollWrap.appendChild(table);
631
+ root.appendChild(scrollWrap);
632
+
633
+ // Pagination
634
+ if (pgCfg) {
635
+ const pgBar = div({ class: 'd-datatable-pagination', role: 'navigation', 'aria-label': 'Table pagination' });
636
+
637
+ const sizeLabel = labelTag({ class: 'd-datatable-page-size' }, 'Rows: ');
638
+ const sizeSelect = h('select', {
639
+ 'aria-label': 'Rows per page',
640
+ onchange(e) {
641
+ batch(() => { setPageSize(Number(e.target.value)); setPage(1); });
642
+ if (pgCfg.onPageChange) pgCfg.onPageChange({ page: 1, pageSize: Number(e.target.value) });
643
+ }
644
+ });
645
+ [10, 20, 50, 100].forEach(n => {
646
+ const opt = h('option', { value: n }, String(n));
647
+ if (n === (pgCfg.pageSize || 10)) opt.selected = true;
648
+ sizeSelect.appendChild(opt);
649
+ });
650
+ sizeLabel.appendChild(sizeSelect);
651
+ pgBar.appendChild(sizeLabel);
652
+
653
+ const pgInfo = span({ class: 'd-datatable-page-info' });
654
+ pgBar.appendChild(pgInfo);
655
+
656
+ const prevBtn = buttonTag({
657
+ type: 'button', class: 'd-datatable-page-btn', 'aria-label': 'Previous page'
658
+ }, caret('left'), ' Prev');
659
+ prevBtn.addEventListener('click', () => {
660
+ const p = page();
661
+ if (p > 1) { setPage(p - 1); if (pgCfg.onPageChange) pgCfg.onPageChange({ page: p - 1, pageSize: pageSize() }); }
662
+ });
663
+
664
+ const nextBtn = buttonTag({
665
+ type: 'button', class: 'd-datatable-page-btn', 'aria-label': 'Next page'
666
+ }, 'Next ', caret('right'));
667
+ nextBtn.addEventListener('click', () => {
668
+ const p = page();
669
+ if (p < getPageCount()) { setPage(p + 1); if (pgCfg.onPageChange) pgCfg.onPageChange({ page: p + 1, pageSize: pageSize() }); }
670
+ });
671
+
672
+ pgBar.appendChild(prevBtn);
673
+ pgBar.appendChild(nextBtn);
674
+
675
+ createEffect(() => {
676
+ const p = page();
677
+ const pc = getPageCount();
678
+ const total = getTotal();
679
+ const ps = pageSize();
680
+ const start = (p - 1) * ps + 1;
681
+ const end = Math.min(p * ps, total);
682
+ pgInfo.textContent = total > 0 ? `${start}\u2013${end} of ${total}` : '0 results';
683
+ prevBtn.disabled = p <= 1;
684
+ nextBtn.disabled = p >= pc;
685
+ });
686
+
687
+ root.appendChild(pgBar);
688
+ }
689
+
690
+ // Pin offsets
691
+ createEffect(() => {
692
+ colWidths();
693
+ const allCols = getEffectiveCols();
694
+ const ths = headerRow.querySelectorAll('.d-datatable-th');
695
+ const extraCols = (expandable ? 1 : 0) + (selection === 'multi' ? 1 : 0);
696
+
697
+ let leftAcc = 0;
698
+ allCols.forEach((col, ci) => {
699
+ if (col.pinned !== 'left') return;
700
+ const thIdx = ci + extraCols;
701
+ const th = ths[thIdx];
702
+ if (!th) return;
703
+ th.style.left = leftAcc + 'px';
704
+ const tds = tbody.querySelectorAll(`tr > .d-datatable-td:nth-child(${thIdx + 1})`);
705
+ tds.forEach(td => { td.style.left = leftAcc + 'px'; });
706
+ leftAcc += th.offsetWidth;
707
+ });
708
+
709
+ let rightAcc = 0;
710
+ for (let ci = allCols.length - 1; ci >= 0; ci--) {
711
+ const col = allCols[ci];
712
+ if (col.pinned !== 'right') continue;
713
+ const thIdx = ci + extraCols;
714
+ const th = ths[thIdx];
715
+ if (!th) continue;
716
+ th.style.right = rightAcc + 'px';
717
+ const tds = tbody.querySelectorAll(`tr > .d-datatable-td:nth-child(${thIdx + 1})`);
718
+ tds.forEach(td => { td.style.right = rightAcc + 'px'; });
719
+ rightAcc += th.offsetWidth;
720
+ }
721
+ });
722
+
723
+ return root;
724
+ }