argus-table 0.0.1 → 0.2.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 (59) hide show
  1. package/README.md +25 -2
  2. package/dist/announce.js +27 -0
  3. package/dist/announce.js.map +1 -0
  4. package/dist/argus-column.js +121 -0
  5. package/dist/argus-column.js.map +1 -0
  6. package/dist/argus-table.js +618 -91
  7. package/dist/argus-table.js.map +1 -1
  8. package/dist/cdn/argus-table.js +87 -4715
  9. package/dist/cdn/argus-table.js.map +1 -1
  10. package/dist/controllers/auto-width.js +89 -0
  11. package/dist/controllers/auto-width.js.map +1 -0
  12. package/dist/controllers/column-children.js +41 -0
  13. package/dist/controllers/column-children.js.map +1 -0
  14. package/dist/controllers/data-controller.js +177 -0
  15. package/dist/controllers/data-controller.js.map +1 -0
  16. package/dist/controllers/pin-scroll-controller.js +78 -0
  17. package/dist/controllers/pin-scroll-controller.js.map +1 -0
  18. package/dist/controllers/reorder-drag.js +160 -0
  19. package/dist/controllers/reorder-drag.js.map +1 -0
  20. package/dist/controllers/resize-drag.js +3 -0
  21. package/dist/controllers/resize-drag.js.map +1 -1
  22. package/dist/controllers/table-controller.js +2 -1
  23. package/dist/controllers/table-controller.js.map +1 -1
  24. package/dist/controllers/virtual-controller.js +146 -9
  25. package/dist/controllers/virtual-controller.js.map +1 -1
  26. package/dist/core/src/columns/column-order.js +50 -0
  27. package/dist/core/src/columns/column-order.js.map +1 -0
  28. package/dist/core/src/columns/resolve-columns.js +5 -1
  29. package/dist/core/src/columns/resolve-columns.js.map +1 -1
  30. package/dist/core/src/data/page-cache.js +143 -0
  31. package/dist/core/src/data/page-cache.js.map +1 -0
  32. package/dist/core/src/grid/row-layout.js +58 -0
  33. package/dist/core/src/grid/row-layout.js.map +1 -0
  34. package/dist/core/src/table/create-table.js +112 -14
  35. package/dist/core/src/table/create-table.js.map +1 -1
  36. package/dist/core/src/table/sorted-row-model.js +86 -0
  37. package/dist/core/src/table/sorted-row-model.js.map +1 -0
  38. package/dist/core/src/virtual/scroll-scale.js +74 -0
  39. package/dist/core/src/virtual/scroll-scale.js.map +1 -0
  40. package/dist/custom-elements.json +1510 -49
  41. package/dist/events.js +5 -1
  42. package/dist/events.js.map +1 -1
  43. package/dist/index.d.ts +252 -27
  44. package/dist/index.js +3 -2
  45. package/dist/max-height.js +68 -0
  46. package/dist/max-height.js.map +1 -0
  47. package/dist/render/body.js +53 -21
  48. package/dist/render/body.js.map +1 -1
  49. package/dist/render/cells.js +21 -4
  50. package/dist/render/cells.js.map +1 -1
  51. package/dist/render/content.js +12 -5
  52. package/dist/render/content.js.map +1 -1
  53. package/dist/render/header.js +21 -5
  54. package/dist/render/header.js.map +1 -1
  55. package/dist/render/pinned-rows.js +29 -0
  56. package/dist/render/pinned-rows.js.map +1 -0
  57. package/dist/styles/base.js +1 -225
  58. package/dist/styles/base.js.map +1 -1
  59. package/package.json +3 -1
@@ -1 +1 @@
1
- {"version":3,"file":"body.js","names":[],"sources":["../../src/render/body.ts"],"sourcesContent":["import { html, nothing } from 'lit';\nimport { repeat } from 'lit/directives/repeat.js';\nimport { styleMap } from 'lit/directives/style-map.js';\nimport type { ColumnView } from '@argus-table/core';\nimport type { WindowRow } from '../controllers/virtual-controller.js';\nimport type { CellContent } from '../types.js';\nimport { cellParts, cellSlot, pinStyle } from './content.js';\n\ntype Columns<T> = readonly ColumnView<T, CellContent>[];\n\n/** The cell holding the grid's tab stop: a row index and a column id. */\nexport interface ActiveCell {\n index: number;\n col: string;\n}\n\n/**\n * The virtualized body. Rows are keyed by pool slot, so a row entering the window\n * reuses the DOM of one that left (ADR 0001), and they stay in pool order: screen\n * readers get the position from `aria-rowindex` (the header row is 1).\n */\nexport function renderBody<T>(\n rows: readonly WindowRow[],\n columns: Columns<T>,\n totalSize: number,\n active: ActiveCell | undefined,\n) {\n return html`<div\n part=\"body\"\n role=\"rowgroup\"\n style=${styleMap({ blockSize: `${String(totalSize)}px` })}\n >\n ${repeat(\n rows,\n (r) => r.slot,\n (r) => renderRow(r, columns, r.index === active?.index ? active.col : undefined),\n )}\n </div>`;\n}\n\n/**\n * One body row. Every cell is focusable (`tabindex=-1`) so clicks focus it; the\n * active one is the grid's single tab stop (roving tabindex).\n */\nfunction renderRow<T>(row: WindowRow, columns: Columns<T>, activeCol: string | undefined) {\n return html`<div\n part=${row.index % 2 === 0 ? 'row row-even' : 'row row-odd'}\n class=\"row\"\n role=\"row\"\n aria-rowindex=${row.index + 2}\n style=${styleMap({ transform: `translateY(${String(row.start)}px)` })}\n >\n ${repeat(\n columns,\n (c) => c.id,\n (c) =>\n html`<div\n part=${cellParts(c)}\n role=\"gridcell\"\n aria-colindex=${c.colIndex}\n tabindex=${c.id === activeCol ? 0 : -1}\n data-col=${c.id}\n data-align=${c.align}\n data-pin=${c.pin ?? nothing}\n ?data-pin-edge=${c.pinEdge}\n style=${pinStyle(c) ?? nothing}\n >\n <slot name=${cellSlot(row.slot, c.colIndex)}></slot>\n </div>`,\n )}\n </div>`;\n}\n"],"mappings":";;;;;;;;;;AAqBA,SAAgB,WACd,MACA,SACA,WACA,QACA;CACA,OAAO,IAAI;;;YAGD,SAAS,EAAE,WAAW,GAAG,OAAO,SAAS,EAAE,IAAI,CAAC,EAAE;;MAExD,OACA,OACC,MAAM,EAAE,OACR,MAAM,UAAU,GAAG,SAAS,EAAE,UAAU,QAAQ,QAAQ,OAAO,MAAM,KAAA,CAAS,CACjF,EAAE;;AAEN;;;;;AAMA,SAAS,UAAa,KAAgB,SAAqB,WAA+B;CACxF,OAAO,IAAI;WACF,IAAI,QAAQ,MAAM,IAAI,iBAAiB,cAAc;;;oBAG5C,IAAI,QAAQ,EAAE;YACtB,SAAS,EAAE,WAAW,cAAc,OAAO,IAAI,KAAK,EAAE,KAAK,CAAC,EAAE;;MAEpE,OACA,UACC,MAAM,EAAE,KACR,MACC,IAAI;iBACK,UAAU,CAAC,EAAE;;0BAEJ,EAAE,SAAS;qBAChB,EAAE,OAAO,YAAY,IAAI,GAAG;qBAC5B,EAAE,GAAG;uBACH,EAAE,MAAM;qBACV,EAAE,OAAO,QAAQ;2BACX,EAAE,QAAQ;kBACnB,SAAS,CAAC,KAAK,QAAQ;;uBAElB,SAAS,IAAI,MAAM,EAAE,QAAQ,EAAE;eAElD,EAAE;;AAEN"}
1
+ {"version":3,"file":"body.js","names":[],"sources":["../../src/render/body.ts"],"sourcesContent":["import { html, nothing } from 'lit';\nimport { repeat } from 'lit/directives/repeat.js';\nimport { styleMap } from 'lit/directives/style-map.js';\nimport type { ColumnView, RowState } from '@argus-table/core';\nimport type { WindowRow } from '../controllers/virtual-controller.js';\nimport type { CellContent } from '../types.js';\nimport { cellParts, cellSlot, pinStyle } from './content.js';\n\ntype Columns<T> = readonly ColumnView<T, CellContent>[];\n\n/** The cell holding the grid's tab stop: a row index and a column id. */\nexport interface ActiveCell {\n index: number;\n col: string;\n}\n\n/** Row loading state for `dataSource` tables. */\nexport interface Loading {\n /** Whether a row's data is loaded, still loading, or its page failed. */\n stateAt(index: number): RowState;\n /** Rows that show a Retry button (one per failed page in view), with its accessible label. */\n retry: ReadonlyMap<number, string>;\n /** Click handler of the Retry buttons; the button carries the row index in `data-row`. */\n onRetry: (event: Event) => void;\n}\n\n/**\n * The virtualized body. Rows are keyed by pool slot, so a row entering the window\n * reuses the DOM of one that left (ADR 0001), and they stay in pool order: screen\n * readers get the position from `aria-rowindex`. `firstRowIndex` is the\n * `aria-rowindex` of row 0: after the header and the rows pinned to the top.\n */\nexport function renderBody<T>(\n rows: readonly WindowRow[],\n columns: Columns<T>,\n totalSize: number,\n active: ActiveCell | undefined,\n firstRowIndex: number,\n loading?: Loading,\n) {\n return html`<div\n part=\"body\"\n role=\"rowgroup\"\n style=${styleMap({ blockSize: `${String(totalSize)}px` })}\n >\n ${repeat(\n rows,\n (r) => r.slot,\n (r) =>\n renderRow(\n r,\n columns,\n r.index === active?.index ? active.col : undefined,\n firstRowIndex,\n loading,\n ),\n )}\n </div>`;\n}\n\n/**\n * One body row. Every cell is focusable (`tabindex=-1`) so clicks focus it; the\n * active one is the grid's single tab stop (roving tabindex). A row whose data\n * isn't loaded shows skeleton bars, and a Retry button if its page failed.\n */\nfunction renderRow<T>(\n row: WindowRow,\n columns: Columns<T>,\n activeCol: string | undefined,\n firstRowIndex: number,\n loading: Loading | undefined,\n) {\n const state = loading?.stateAt(row.index) ?? 'loaded';\n let parts = row.index % 2 === 0 ? 'row row-even' : 'row row-odd';\n if (state !== 'loaded') parts += ' row-loading';\n const retry = state === 'error' ? loading?.retry.get(row.index) : undefined;\n return html`<div\n part=${parts}\n class=\"row\"\n role=\"row\"\n aria-rowindex=${row.index + firstRowIndex}\n aria-busy=${state === 'loading' ? 'true' : nothing}\n style=${styleMap({ transform: `translateY(${String(row.start)}px)` })}\n >\n ${repeat(\n columns,\n (c) => c.id,\n (c) =>\n renderCell(\n c,\n c.id === activeCol,\n cellSlot(row.slot, c.colIndex),\n false,\n placeholder(state, c.colIndex === 1 ? retry : undefined, row.index, loading),\n ),\n )}\n </div>`;\n}\n\n/**\n * A body or pinned-row cell. Every cell is focusable (`tabindex=-1`) so clicks\n * focus it; `tabStop` marks the grid's single tab stop. `before` goes ahead of\n * the slot (a loading row's skeleton bar or Retry button).\n */\nexport function renderCell<T>(\n c: ColumnView<T, CellContent>,\n tabStop: boolean,\n slot: string,\n pinnedRow: boolean,\n before: unknown = nothing,\n) {\n return html`<div\n part=${cellParts(c, pinnedRow)}\n role=\"gridcell\"\n aria-colindex=${c.colIndex}\n tabindex=${tabStop ? 0 : -1}\n data-col=${c.id}\n data-align=${c.align}\n data-pin=${c.pin ?? nothing}\n style=${pinStyle(c) ?? nothing}\n >\n ${before}<slot name=${slot}></slot>\n </div>`;\n}\n\n/** What an unloaded cell shows: a skeleton bar, or the Retry button in a failed page's first cell. */\nfunction placeholder(\n state: RowState,\n retryLabel: string | undefined,\n index: number,\n loading: Loading | undefined,\n) {\n if (state === 'loaded') return nothing;\n if (retryLabel !== undefined && loading) {\n return html`<button\n part=\"retry\"\n type=\"button\"\n tabindex=\"-1\"\n data-row=${index}\n aria-label=${retryLabel}\n @click=${loading.onRetry}\n >\n Retry\n </button>`;\n }\n return html`<div\n part=${state === 'error' ? 'skeleton skeleton-error' : 'skeleton'}\n aria-hidden=\"true\"\n ></div>`;\n}\n"],"mappings":";;;;;;;;;;;AAgCA,SAAgB,WACd,MACA,SACA,WACA,QACA,eACA,SACA;CACA,OAAO,IAAI;;;YAGD,SAAS,EAAE,WAAW,GAAG,OAAO,SAAS,EAAE,IAAI,CAAC,EAAE;;MAExD,OACA,OACC,MAAM,EAAE,OACR,MACC,UACE,GACA,SACA,EAAE,UAAU,QAAQ,QAAQ,OAAO,MAAM,KAAA,GACzC,eACA,OACF,CACJ,EAAE;;AAEN;;;;;;AAOA,SAAS,UACP,KACA,SACA,WACA,eACA,SACA;CACA,MAAM,QAAQ,SAAS,QAAQ,IAAI,KAAK,KAAK;CAC7C,IAAI,QAAQ,IAAI,QAAQ,MAAM,IAAI,iBAAiB;CACnD,IAAI,UAAU,UAAU,SAAS;CACjC,MAAM,QAAQ,UAAU,UAAU,SAAS,MAAM,IAAI,IAAI,KAAK,IAAI,KAAA;CAClE,OAAO,IAAI;WACF,MAAM;;;oBAGG,IAAI,QAAQ,cAAc;gBAC9B,UAAU,YAAY,SAAS,QAAQ;YAC3C,SAAS,EAAE,WAAW,cAAc,OAAO,IAAI,KAAK,EAAE,KAAK,CAAC,EAAE;;MAEpE,OACA,UACC,MAAM,EAAE,KACR,MACC,WACE,GACA,EAAE,OAAO,WACT,SAAS,IAAI,MAAM,EAAE,QAAQ,GAC7B,OACA,YAAY,OAAO,EAAE,aAAa,IAAI,QAAQ,KAAA,GAAW,IAAI,OAAO,OAAO,CAC7E,CACJ,EAAE;;AAEN;;;;;;AAOA,SAAgB,WACd,GACA,SACA,MACA,WACA,SAAkB,SAClB;CACA,OAAO,IAAI;WACF,UAAU,GAAG,SAAS,EAAE;;oBAEf,EAAE,SAAS;eAChB,UAAU,IAAI,GAAG;eACjB,EAAE,GAAG;iBACH,EAAE,MAAM;eACV,EAAE,OAAO,QAAQ;YACpB,SAAS,CAAC,KAAK,QAAQ;;MAE7B,OAAO,aAAa,KAAK;;AAE/B;;AAGA,SAAS,YACP,OACA,YACA,OACA,SACA;CACA,IAAI,UAAU,UAAU,OAAO;CAC/B,IAAI,eAAe,KAAA,KAAa,SAC9B,OAAO,IAAI;;;;iBAIE,MAAM;mBACJ,WAAW;eACf,QAAQ,QAAQ;;;;CAK7B,OAAO,IAAI;WACF,UAAU,UAAU,4BAA4B,WAAW;;;AAGtE"}
@@ -1,20 +1,37 @@
1
- import { cellSlot, formatValue, headerSlot, toChild } from "./content.js";
1
+ import { cellSlot, formatValue, headerSlot, pinnedSlot, toChild } from "./content.js";
2
2
  import { html, render } from "lit";
3
3
  import { repeat } from "lit/directives/repeat.js";
4
4
  // src/render/cells.ts
5
5
  /**
6
6
  * Renders header and cell content into the host's light DOM, as direct children
7
7
  * (a slot only takes the host's own children). Keyed and ordered by pool slot,
8
- * so recycled content updates in place instead of moving (ADR 0001).
8
+ * so recycled content updates in place instead of moving (ADR 0001). The part
9
+ * ends at `renderBefore`, so the host's other children (`<argus-column>`) stay
10
+ * outside it, wherever they are added.
9
11
  */
10
- function renderLightDom(host, model, rows) {
12
+ function renderLightDom(host, model, rows, renderBefore) {
11
13
  const columns = model.columns;
12
- render(html`${repeat(columns, (c) => c.id, (c) => html`<div slot=${headerSlot(c.colIndex)}>${headerContent(c)}</div>`)}${repeat(rows, (r) => r.slot, (r) => repeat(columns, (c) => c.id, (c) => html`<div slot=${cellSlot(r.slot, c.colIndex)}>${cellContent(model, r.index, c)}</div>`))}`, host, { host });
14
+ render(html`${repeat(columns, (c) => c.id, (c) => html`<div slot=${headerSlot(c.colIndex)}>${headerContent(c)}</div>`)}${pinnedContent(model, "top")}${repeat(rows, (r) => r.slot, (r) => repeat(columns, (c) => c.id, (c) => html`<div slot=${cellSlot(r.slot, c.colIndex)}>${cellContent(model, r.index, c)}</div>`))}${pinnedContent(model, "bottom")}`, host, {
15
+ host,
16
+ renderBefore
17
+ });
13
18
  }
14
19
  function headerContent(column) {
15
20
  const { header } = column;
16
21
  return toChild(typeof header === "function" ? header() : header);
17
22
  }
23
+ /** Content of the rows pinned to one side, slotted by side, index and column. */
24
+ function pinnedContent(model, side) {
25
+ const columns = model.columns;
26
+ return Array.from({ length: model.pinnedCount(side) }, (_, i) => i).map((i) => repeat(columns, (c) => c.id, (c) => html`<div slot=${pinnedSlot(side, i, c.colIndex)}>${pinnedCell(model, side, i, c)}</div>`));
27
+ }
28
+ function pinnedCell(model, side, index, column) {
29
+ if (column.cell) {
30
+ const context = model.pinnedCellContext(side, index, column.id);
31
+ return context ? toChild(column.cell(context.row, context)) : "";
32
+ }
33
+ return column.hasAccessor ? formatValue(model.pinnedValueAt(side, index, column.id)) : "";
34
+ }
18
35
  function cellContent(model, index, column) {
19
36
  if (column.cell) {
20
37
  const context = model.cellContext(index, column.id);
@@ -1 +1 @@
1
- {"version":3,"file":"cells.js","names":[],"sources":["../../src/render/cells.ts"],"sourcesContent":["import { html, render } from 'lit';\nimport { repeat } from 'lit/directives/repeat.js';\nimport type { ColumnView, TableModel } from '@argus-table/core';\nimport type { WindowRow } from '../controllers/virtual-controller.js';\nimport type { CellContent } from '../types.js';\nimport { cellSlot, formatValue, headerSlot, toChild } from './content.js';\n\ntype Column<T> = ColumnView<T, CellContent>;\n\n/**\n * Renders header and cell content into the host's light DOM, as direct children\n * (a slot only takes the host's own children). Keyed and ordered by pool slot,\n * so recycled content updates in place instead of moving (ADR 0001).\n */\nexport function renderLightDom<T>(\n host: HTMLElement,\n model: TableModel<T, CellContent>,\n rows: readonly WindowRow[],\n): void {\n const columns = model.columns;\n render(\n html`${repeat(\n columns,\n (c) => c.id,\n (c) => html`<div slot=${headerSlot(c.colIndex)}>${headerContent(c)}</div>`,\n )}${repeat(\n rows,\n (r) => r.slot,\n (r) =>\n repeat(\n columns,\n (c) => c.id,\n (c) =>\n html`<div slot=${cellSlot(r.slot, c.colIndex)}>${cellContent(model, r.index, c)}</div>`,\n ),\n )}`,\n host,\n { host },\n );\n}\n\nfunction headerContent<T>(column: Column<T>): unknown {\n const { header } = column;\n return toChild(typeof header === 'function' ? header() : header);\n}\n\nfunction cellContent<T>(model: TableModel<T, CellContent>, index: number, column: Column<T>) {\n if (column.cell) {\n const context = model.cellContext(index, column.id);\n return context ? toChild(column.cell(context.row, context)) : '';\n }\n return column.hasAccessor ? formatValue(model.valueAt(index, column.id)) : '';\n}\n"],"mappings":";;;;;;;;;AAcA,SAAgB,eACd,MACA,OACA,MACM;CACN,MAAM,UAAU,MAAM;CACtB,OACE,IAAI,GAAG,OACL,UACC,MAAM,EAAE,KACR,MAAM,IAAI,aAAa,WAAW,EAAE,QAAQ,EAAE,GAAG,cAAc,CAAC,EAAE,OACrE,IAAI,OACF,OACC,MAAM,EAAE,OACR,MACC,OACE,UACC,MAAM,EAAE,KACR,MACC,IAAI,aAAa,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,YAAY,OAAO,EAAE,OAAO,CAAC,EAAE,OACpF,CACJ,KACA,MACA,EAAE,KAAK,CACT;AACF;AAEA,SAAS,cAAiB,QAA4B;CACpD,MAAM,EAAE,WAAW;CACnB,OAAO,QAAQ,OAAO,WAAW,aAAa,OAAO,IAAI,MAAM;AACjE;AAEA,SAAS,YAAe,OAAmC,OAAe,QAAmB;CAC3F,IAAI,OAAO,MAAM;EACf,MAAM,UAAU,MAAM,YAAY,OAAO,OAAO,EAAE;EAClD,OAAO,UAAU,QAAQ,OAAO,KAAK,QAAQ,KAAK,OAAO,CAAC,IAAI;CAChE;CACA,OAAO,OAAO,cAAc,YAAY,MAAM,QAAQ,OAAO,OAAO,EAAE,CAAC,IAAI;AAC7E"}
1
+ {"version":3,"file":"cells.js","names":[],"sources":["../../src/render/cells.ts"],"sourcesContent":["import { html, render } from 'lit';\nimport { repeat } from 'lit/directives/repeat.js';\nimport type { ColumnView, RowPin, TableModel } from '@argus-table/core';\nimport type { WindowRow } from '../controllers/virtual-controller.js';\nimport type { CellContent } from '../types.js';\nimport { cellSlot, formatValue, headerSlot, pinnedSlot, toChild } from './content.js';\n\ntype Column<T> = ColumnView<T, CellContent>;\n\n/**\n * Renders header and cell content into the host's light DOM, as direct children\n * (a slot only takes the host's own children). Keyed and ordered by pool slot,\n * so recycled content updates in place instead of moving (ADR 0001). The part\n * ends at `renderBefore`, so the host's other children (`<argus-column>`) stay\n * outside it, wherever they are added.\n */\nexport function renderLightDom<T>(\n host: HTMLElement,\n model: TableModel<T, CellContent>,\n rows: readonly WindowRow[],\n renderBefore: ChildNode,\n): void {\n const columns = model.columns;\n render(\n html`${repeat(\n columns,\n (c) => c.id,\n (c) => html`<div slot=${headerSlot(c.colIndex)}>${headerContent(c)}</div>`,\n )}${pinnedContent(model, 'top')}${repeat(\n rows,\n (r) => r.slot,\n (r) =>\n repeat(\n columns,\n (c) => c.id,\n (c) =>\n html`<div slot=${cellSlot(r.slot, c.colIndex)}>${cellContent(model, r.index, c)}</div>`,\n ),\n )}${pinnedContent(model, 'bottom')}`,\n host,\n { host, renderBefore },\n );\n}\n\nfunction headerContent<T>(column: Column<T>): unknown {\n const { header } = column;\n return toChild(typeof header === 'function' ? header() : header);\n}\n\n/** Content of the rows pinned to one side, slotted by side, index and column. */\nfunction pinnedContent<T>(model: TableModel<T, CellContent>, side: RowPin) {\n const columns = model.columns;\n const rows = Array.from({ length: model.pinnedCount(side) }, (_, i) => i);\n return rows.map((i) =>\n repeat(\n columns,\n (c) => c.id,\n (c) =>\n html`<div slot=${pinnedSlot(side, i, c.colIndex)}>${pinnedCell(model, side, i, c)}</div>`,\n ),\n );\n}\n\nfunction pinnedCell<T>(\n model: TableModel<T, CellContent>,\n side: RowPin,\n index: number,\n column: Column<T>,\n) {\n if (column.cell) {\n const context = model.pinnedCellContext(side, index, column.id);\n return context ? toChild(column.cell(context.row, context)) : '';\n }\n return column.hasAccessor ? formatValue(model.pinnedValueAt(side, index, column.id)) : '';\n}\n\nfunction cellContent<T>(model: TableModel<T, CellContent>, index: number, column: Column<T>) {\n if (column.cell) {\n const context = model.cellContext(index, column.id);\n return context ? toChild(column.cell(context.row, context)) : '';\n }\n return column.hasAccessor ? formatValue(model.valueAt(index, column.id)) : '';\n}\n"],"mappings":";;;;;;;;;;;AAgBA,SAAgB,eACd,MACA,OACA,MACA,cACM;CACN,MAAM,UAAU,MAAM;CACtB,OACE,IAAI,GAAG,OACL,UACC,MAAM,EAAE,KACR,MAAM,IAAI,aAAa,WAAW,EAAE,QAAQ,EAAE,GAAG,cAAc,CAAC,EAAE,OACrE,IAAI,cAAc,OAAO,KAAK,IAAI,OAChC,OACC,MAAM,EAAE,OACR,MACC,OACE,UACC,MAAM,EAAE,KACR,MACC,IAAI,aAAa,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,YAAY,OAAO,EAAE,OAAO,CAAC,EAAE,OACpF,CACJ,IAAI,cAAc,OAAO,QAAQ,KACjC,MACA;EAAE;EAAM;CAAa,CACvB;AACF;AAEA,SAAS,cAAiB,QAA4B;CACpD,MAAM,EAAE,WAAW;CACnB,OAAO,QAAQ,OAAO,WAAW,aAAa,OAAO,IAAI,MAAM;AACjE;;AAGA,SAAS,cAAiB,OAAmC,MAAc;CACzE,MAAM,UAAU,MAAM;CAEtB,OADa,MAAM,KAAK,EAAE,QAAQ,MAAM,YAAY,IAAI,EAAE,IAAI,GAAG,MAAM,CAChE,CAAA,CAAK,KAAK,MACf,OACE,UACC,MAAM,EAAE,KACR,MACC,IAAI,aAAa,WAAW,MAAM,GAAG,EAAE,QAAQ,EAAE,GAAG,WAAW,OAAO,MAAM,GAAG,CAAC,EAAE,OACtF,CACF;AACF;AAEA,SAAS,WACP,OACA,MACA,OACA,QACA;CACA,IAAI,OAAO,MAAM;EACf,MAAM,UAAU,MAAM,kBAAkB,MAAM,OAAO,OAAO,EAAE;EAC9D,OAAO,UAAU,QAAQ,OAAO,KAAK,QAAQ,KAAK,OAAO,CAAC,IAAI;CAChE;CACA,OAAO,OAAO,cAAc,YAAY,MAAM,cAAc,MAAM,OAAO,OAAO,EAAE,CAAC,IAAI;AACzF;AAEA,SAAS,YAAe,OAAmC,OAAe,QAAmB;CAC3F,IAAI,OAAO,MAAM;EACf,MAAM,UAAU,MAAM,YAAY,OAAO,OAAO,EAAE;EAClD,OAAO,UAAU,QAAQ,OAAO,KAAK,QAAQ,KAAK,OAAO,CAAC,IAAI;CAChE;CACA,OAAO,OAAO,cAAc,YAAY,MAAM,QAAQ,OAAO,OAAO,EAAE,CAAC,IAAI;AAC7E"}
@@ -11,12 +11,19 @@ function formatValue(value) {
11
11
  if (value instanceof Date) return value.toLocaleString();
12
12
  return typeof value === "object" ? JSON.stringify(value) : String(value);
13
13
  }
14
- /** Private slot names (ADR 0001): cells by pool slot and column, headers by column. */
14
+ /**
15
+ * Private slot names (ADR 0001): cells by pool slot and column, headers by
16
+ * column, pinned rows by side, index and column.
17
+ */
15
18
  var cellSlot = (slot, colIndex) => `c-${String(slot)}-${String(colIndex)}`;
16
19
  var headerSlot = (colIndex) => `h-${String(colIndex)}`;
17
- /** Part list of a body cell: state is in the part name (`::part()` can't take attribute selectors). */
18
- function cellParts(column) {
19
- let parts = "cell";
20
+ var pinnedSlot = (side, index, colIndex) => `${side === "top" ? "t" : "b"}-${String(index)}-${String(colIndex)}`;
21
+ /**
22
+ * Part list of a body or pinned-row cell: state is in the part name (`::part()`
23
+ * can't take attribute selectors).
24
+ */
25
+ function cellParts(column, pinnedRow = false) {
26
+ let parts = pinnedRow ? "cell cell-pinned-row" : "cell";
20
27
  if (column.pin) parts += column.pinEdge ? " cell-pinned cell-pinned-edge" : " cell-pinned";
21
28
  if (column.sorted) parts += " cell-sorted";
22
29
  return parts;
@@ -30,6 +37,6 @@ function ariaSort(sorted) {
30
37
  function pinStyle(column) {
31
38
  return column.pin ? `--_pin-offset: ${String(column.offset)}px` : void 0;
32
39
  }
33
- export { ariaSort, cellParts, cellSlot, formatValue, headerSlot, pinStyle, toChild };
40
+ export { ariaSort, cellParts, cellSlot, formatValue, headerSlot, pinStyle, pinnedSlot, toChild };
34
41
 
35
42
  //# sourceMappingURL=content.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"content.js","names":[],"sources":["../../src/render/content.ts"],"sourcesContent":["import { nothing } from 'lit';\nimport type { CellContent } from '../types.js';\n\n/** Turns header or cell content into a Lit child. */\nexport function toChild(content: CellContent | null | undefined): unknown {\n if (content === null || content === undefined) return nothing;\n return typeof content === 'number' ? String(content) : content;\n}\n\n/** Default cell text for an accessor value. */\nexport function formatValue(value: unknown): string {\n if (value === null || value === undefined) return '';\n if (value instanceof Date) return value.toLocaleString();\n // eslint-disable-next-line @typescript-eslint/no-base-to-string -- objects are handled above\n return typeof value === 'object' ? JSON.stringify(value) : String(value);\n}\n\n/** Private slot names (ADR 0001): cells by pool slot and column, headers by column. */\nexport const cellSlot = (slot: number, colIndex: number) => `c-${String(slot)}-${String(colIndex)}`;\nexport const headerSlot = (colIndex: number) => `h-${String(colIndex)}`;\n\ninterface Pinnable {\n pin: 'start' | 'end' | undefined;\n pinEdge: boolean;\n}\n\n/** Part list of a body cell: state is in the part name (`::part()` can't take attribute selectors). */\nexport function cellParts(column: Pinnable & { sorted: 'asc' | 'desc' | false }): string {\n let parts = 'cell';\n if (column.pin) parts += column.pinEdge ? ' cell-pinned cell-pinned-edge' : ' cell-pinned';\n if (column.sorted) parts += ' cell-sorted';\n return parts;\n}\n\n/** `aria-sort` for a header: only the sorted column carries it (WAI-ARIA APG). */\nexport function ariaSort(sorted: 'asc' | 'desc' | false): 'ascending' | 'descending' | undefined {\n if (!sorted) return undefined;\n return sorted === 'asc' ? 'ascending' : 'descending';\n}\n\n/** Inline style of a pinned cell: its sticky inset, from the start or the end edge. */\nexport function pinStyle(column: Pinnable & { offset: number }): string | undefined {\n return column.pin ? `--_pin-offset: ${String(column.offset)}px` : undefined;\n}\n"],"mappings":";;;AAIA,SAAgB,QAAQ,SAAkD;CACxE,IAAI,YAAY,QAAQ,YAAY,KAAA,GAAW,OAAO;CACtD,OAAO,OAAO,YAAY,WAAW,OAAO,OAAO,IAAI;AACzD;;AAGA,SAAgB,YAAY,OAAwB;CAClD,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW,OAAO;CAClD,IAAI,iBAAiB,MAAM,OAAO,MAAM,eAAe;CAEvD,OAAO,OAAO,UAAU,WAAW,KAAK,UAAU,KAAK,IAAI,OAAO,KAAK;AACzE;;AAGA,IAAa,YAAY,MAAc,aAAqB,KAAK,OAAO,IAAI,EAAE,GAAG,OAAO,QAAQ;AAChG,IAAa,cAAc,aAAqB,KAAK,OAAO,QAAQ;;AAQpE,SAAgB,UAAU,QAA+D;CACvF,IAAI,QAAQ;CACZ,IAAI,OAAO,KAAK,SAAS,OAAO,UAAU,kCAAkC;CAC5E,IAAI,OAAO,QAAQ,SAAS;CAC5B,OAAO;AACT;;AAGA,SAAgB,SAAS,QAAwE;CAC/F,IAAI,CAAC,QAAQ,OAAO,KAAA;CACpB,OAAO,WAAW,QAAQ,cAAc;AAC1C;;AAGA,SAAgB,SAAS,QAA2D;CAClF,OAAO,OAAO,MAAM,kBAAkB,OAAO,OAAO,MAAM,EAAE,MAAM,KAAA;AACpE"}
1
+ {"version":3,"file":"content.js","names":[],"sources":["../../src/render/content.ts"],"sourcesContent":["import { nothing } from 'lit';\nimport type { CellContent } from '../types.js';\n\n/** Turns header or cell content into a Lit child. */\nexport function toChild(content: CellContent | null | undefined): unknown {\n if (content === null || content === undefined) return nothing;\n return typeof content === 'number' ? String(content) : content;\n}\n\n/** Default cell text for an accessor value. */\nexport function formatValue(value: unknown): string {\n if (value === null || value === undefined) return '';\n if (value instanceof Date) return value.toLocaleString();\n // eslint-disable-next-line @typescript-eslint/no-base-to-string -- objects are handled above\n return typeof value === 'object' ? JSON.stringify(value) : String(value);\n}\n\n/**\n * Private slot names (ADR 0001): cells by pool slot and column, headers by\n * column, pinned rows by side, index and column.\n */\nexport const cellSlot = (slot: number, colIndex: number) => `c-${String(slot)}-${String(colIndex)}`;\nexport const headerSlot = (colIndex: number) => `h-${String(colIndex)}`;\nexport const pinnedSlot = (side: 'top' | 'bottom', index: number, colIndex: number) =>\n `${side === 'top' ? 't' : 'b'}-${String(index)}-${String(colIndex)}`;\n\ninterface Pinnable {\n pin: 'start' | 'end' | undefined;\n pinEdge: boolean;\n}\n\n/**\n * Part list of a body or pinned-row cell: state is in the part name (`::part()`\n * can't take attribute selectors).\n */\nexport function cellParts(\n column: Pinnable & { sorted: 'asc' | 'desc' | false },\n pinnedRow = false,\n): string {\n let parts = pinnedRow ? 'cell cell-pinned-row' : 'cell';\n if (column.pin) parts += column.pinEdge ? ' cell-pinned cell-pinned-edge' : ' cell-pinned';\n if (column.sorted) parts += ' cell-sorted';\n return parts;\n}\n\n/** `aria-sort` for a header: only the sorted column carries it (WAI-ARIA APG). */\nexport function ariaSort(sorted: 'asc' | 'desc' | false): 'ascending' | 'descending' | undefined {\n if (!sorted) return undefined;\n return sorted === 'asc' ? 'ascending' : 'descending';\n}\n\n/** Inline style of a pinned cell: its sticky inset, from the start or the end edge. */\nexport function pinStyle(column: Pinnable & { offset: number }): string | undefined {\n return column.pin ? `--_pin-offset: ${String(column.offset)}px` : undefined;\n}\n"],"mappings":";;;AAIA,SAAgB,QAAQ,SAAkD;CACxE,IAAI,YAAY,QAAQ,YAAY,KAAA,GAAW,OAAO;CACtD,OAAO,OAAO,YAAY,WAAW,OAAO,OAAO,IAAI;AACzD;;AAGA,SAAgB,YAAY,OAAwB;CAClD,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW,OAAO;CAClD,IAAI,iBAAiB,MAAM,OAAO,MAAM,eAAe;CAEvD,OAAO,OAAO,UAAU,WAAW,KAAK,UAAU,KAAK,IAAI,OAAO,KAAK;AACzE;;;;;AAMA,IAAa,YAAY,MAAc,aAAqB,KAAK,OAAO,IAAI,EAAE,GAAG,OAAO,QAAQ;AAChG,IAAa,cAAc,aAAqB,KAAK,OAAO,QAAQ;AACpE,IAAa,cAAc,MAAwB,OAAe,aAChE,GAAG,SAAS,QAAQ,MAAM,IAAI,GAAG,OAAO,KAAK,EAAE,GAAG,OAAO,QAAQ;;;;;AAWnE,SAAgB,UACd,QACA,YAAY,OACJ;CACR,IAAI,QAAQ,YAAY,yBAAyB;CACjD,IAAI,OAAO,KAAK,SAAS,OAAO,UAAU,kCAAkC;CAC5E,IAAI,OAAO,QAAQ,SAAS;CAC5B,OAAO;AACT;;AAGA,SAAgB,SAAS,QAAwE;CAC/F,IAAI,CAAC,QAAQ,OAAO,KAAA;CACpB,OAAO,WAAW,QAAQ,cAAc;AAC1C;;AAGA,SAAgB,SAAS,QAA2D;CAClF,OAAO,OAAO,MAAM,kBAAkB,OAAO,OAAO,MAAM,EAAE,MAAM,KAAA;AACpE"}
@@ -8,10 +8,15 @@ import { repeat } from "lit/directives/repeat.js";
8
8
  * Every header carries `data-col` (its column id) and sortable ones also
9
9
  * `data-sort-col`; the element's delegated handlers read them. Headers take part
10
10
  * in the grid's roving tabindex: the active one is the tab stop (`activeCol`).
11
- * Resizable columns get a `resize-handle` on the header's inline-end edge. It is
12
- * not focusable: the focused header resizes with Alt+Arrow.
11
+ * Resizable columns get a `resize-handle` on the header's inline-end edge; a
12
+ * double-click on it fits the column to its content. It is not focusable: the
13
+ * focused header resizes with Alt+Arrow.
14
+ *
15
+ * Reorderable columns carry `data-move-col`: a drag on the header moves the
16
+ * column. While one is dragged (`drag`), its header adds `header-cell-dragging`
17
+ * and the target header shows a `drop-indicator` on the edge it would land at.
13
18
  */
14
- function renderHeader(columns, handlers, activeCol) {
19
+ function renderHeader(columns, handlers, activeCol, drag) {
15
20
  return html`<div part="header" role="rowgroup">
16
21
  <div
17
22
  part="header-row"
@@ -20,27 +25,38 @@ function renderHeader(columns, handlers, activeCol) {
20
25
  aria-rowindex="1"
21
26
  @click=${handlers.click}
22
27
  @pointerdown=${handlers.pointerdown}
28
+ @dblclick=${handlers.dblclick}
23
29
  >
24
30
  ${repeat(columns, (c) => c.id, (c) => html`<div
25
- part=${c.sorted ? "header-cell header-cell-sorted" : "header-cell"}
31
+ part=${headerParts(c, drag)}
26
32
  role="columnheader"
27
33
  aria-colindex=${c.colIndex}
28
34
  aria-sort=${ariaSort(c.sorted) ?? nothing}
29
35
  data-col=${c.id}
30
36
  data-sort-col=${c.sortable ? c.id : nothing}
37
+ data-move-col=${c.reorderable ? c.id : nothing}
31
38
  tabindex=${c.id === activeCol ? 0 : -1}
32
39
  data-align=${c.align}
33
40
  data-pin=${c.pin ?? nothing}
34
- ?data-pin-edge=${c.pinEdge}
35
41
  style=${pinStyle(c) ?? nothing}
36
42
  >
37
43
  <slot name=${headerSlot(c.colIndex)}></slot>
38
44
  ${c.sortable ? html`<span class="sort-indicator" aria-hidden="true"></span>` : nothing}
39
45
  ${c.resizable ? resizeHandle(c) : nothing}
46
+ ${drag?.target?.columnId === c.id ? html`<span
47
+ part="drop-indicator"
48
+ data-edge=${drag.target.edge}
49
+ aria-hidden="true"
50
+ ></span>` : nothing}
40
51
  </div>`)}
41
52
  </div>
42
53
  </div>`;
43
54
  }
55
+ function headerParts(c, drag) {
56
+ let parts = c.sorted ? "header-cell header-cell-sorted" : "header-cell";
57
+ if (drag?.columnId === c.id) parts += " header-cell-dragging";
58
+ return parts;
59
+ }
44
60
  /**
45
61
  * A vertical separator with the column's width as its value. A flex column has no
46
62
  * fixed width until it is resized, so it reports none.
@@ -1 +1 @@
1
- {"version":3,"file":"header.js","names":[],"sources":["../../src/render/header.ts"],"sourcesContent":["import { html, nothing } from 'lit';\nimport { repeat } from 'lit/directives/repeat.js';\nimport type { ColumnView } from '@argus-table/core';\nimport type { CellContent } from '../types.js';\nimport { ariaSort, headerSlot, pinStyle } from './content.js';\n\nexport interface HeaderHandlers {\n click: (event: MouseEvent) => void;\n pointerdown: (event: PointerEvent) => void;\n}\n\n/**\n * The sticky header row. Header content is slotted from the light DOM. Pinned\n * header cells use the same private `data-pin` hooks as body cells, but no parts.\n * Every header carries `data-col` (its column id) and sortable ones also\n * `data-sort-col`; the element's delegated handlers read them. Headers take part\n * in the grid's roving tabindex: the active one is the tab stop (`activeCol`).\n * Resizable columns get a `resize-handle` on the header's inline-end edge. It is\n * not focusable: the focused header resizes with Alt+Arrow.\n */\nexport function renderHeader<T>(\n columns: readonly ColumnView<T, CellContent>[],\n handlers: HeaderHandlers,\n activeCol: string | undefined,\n) {\n return html`<div part=\"header\" role=\"rowgroup\">\n <div\n part=\"header-row\"\n class=\"row\"\n role=\"row\"\n aria-rowindex=\"1\"\n @click=${handlers.click}\n @pointerdown=${handlers.pointerdown}\n >\n ${repeat(\n columns,\n (c) => c.id,\n (c) =>\n html`<div\n part=${c.sorted ? 'header-cell header-cell-sorted' : 'header-cell'}\n role=\"columnheader\"\n aria-colindex=${c.colIndex}\n aria-sort=${ariaSort(c.sorted) ?? nothing}\n data-col=${c.id}\n data-sort-col=${c.sortable ? c.id : nothing}\n tabindex=${c.id === activeCol ? 0 : -1}\n data-align=${c.align}\n data-pin=${c.pin ?? nothing}\n ?data-pin-edge=${c.pinEdge}\n style=${pinStyle(c) ?? nothing}\n >\n <slot name=${headerSlot(c.colIndex)}></slot>\n ${c.sortable ? html`<span class=\"sort-indicator\" aria-hidden=\"true\"></span>` : nothing}\n ${c.resizable ? resizeHandle(c) : nothing}\n </div>`,\n )}\n </div>\n </div>`;\n}\n\n/**\n * A vertical separator with the column's width as its value. A flex column has no\n * fixed width until it is resized, so it reports none.\n */\nfunction resizeHandle<T>(c: ColumnView<T, CellContent>) {\n return html`<div\n part=\"resize-handle\"\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label=\"Resize column\"\n aria-valuenow=${c.flex === undefined ? Math.round(c.width) : nothing}\n aria-valuemin=${c.minWidth}\n aria-valuemax=${Number.isFinite(c.maxWidth) ? c.maxWidth : nothing}\n data-resize-col=${c.id}\n ></div>`;\n}\n"],"mappings":";;;;;;;;;;;;;AAoBA,SAAgB,aACd,SACA,UACA,WACA;CACA,OAAO,IAAI;;;;;;eAME,SAAS,MAAM;qBACT,SAAS,YAAY;;QAElC,OACA,UACC,MAAM,EAAE,KACR,MACC,IAAI;mBACK,EAAE,SAAS,mCAAmC,cAAc;;4BAEnD,EAAE,SAAS;wBACf,SAAS,EAAE,MAAM,KAAK,QAAQ;uBAC/B,EAAE,GAAG;4BACA,EAAE,WAAW,EAAE,KAAK,QAAQ;uBACjC,EAAE,OAAO,YAAY,IAAI,GAAG;yBAC1B,EAAE,MAAM;uBACV,EAAE,OAAO,QAAQ;6BACX,EAAE,QAAQ;oBACnB,SAAS,CAAC,KAAK,QAAQ;;yBAElB,WAAW,EAAE,QAAQ,EAAE;cAClC,EAAE,WAAW,IAAI,4DAA4D,QAAQ;cACrF,EAAE,YAAY,aAAa,CAAC,IAAI,QAAQ;iBAEhD,EAAE;;;AAGR;;;;;AAMA,SAAS,aAAgB,GAA+B;CACtD,OAAO,IAAI;;;;;oBAKO,EAAE,SAAS,KAAA,IAAY,KAAK,MAAM,EAAE,KAAK,IAAI,QAAQ;oBACrD,EAAE,SAAS;oBACX,OAAO,SAAS,EAAE,QAAQ,IAAI,EAAE,WAAW,QAAQ;sBACjD,EAAE,GAAG;;AAE3B"}
1
+ {"version":3,"file":"header.js","names":[],"sources":["../../src/render/header.ts"],"sourcesContent":["import { html, nothing } from 'lit';\nimport { repeat } from 'lit/directives/repeat.js';\nimport type { ColumnView } from '@argus-table/core';\nimport type { DropTarget } from '../controllers/reorder-drag.js';\nimport type { CellContent } from '../types.js';\nimport { ariaSort, headerSlot, pinStyle } from './content.js';\n\n/** A column header being dragged, and where it would land (`undefined`: where it is). */\nexport interface HeaderDrag {\n columnId: string;\n target: DropTarget | undefined;\n}\n\nexport interface HeaderHandlers {\n click: (event: MouseEvent) => void;\n pointerdown: (event: PointerEvent) => void;\n dblclick: (event: MouseEvent) => void;\n}\n\n/**\n * The sticky header row. Header content is slotted from the light DOM. Pinned\n * header cells use the same private `data-pin` hooks as body cells, but no parts.\n * Every header carries `data-col` (its column id) and sortable ones also\n * `data-sort-col`; the element's delegated handlers read them. Headers take part\n * in the grid's roving tabindex: the active one is the tab stop (`activeCol`).\n * Resizable columns get a `resize-handle` on the header's inline-end edge; a\n * double-click on it fits the column to its content. It is not focusable: the\n * focused header resizes with Alt+Arrow.\n *\n * Reorderable columns carry `data-move-col`: a drag on the header moves the\n * column. While one is dragged (`drag`), its header adds `header-cell-dragging`\n * and the target header shows a `drop-indicator` on the edge it would land at.\n */\nexport function renderHeader<T>(\n columns: readonly ColumnView<T, CellContent>[],\n handlers: HeaderHandlers,\n activeCol: string | undefined,\n drag?: HeaderDrag,\n) {\n return html`<div part=\"header\" role=\"rowgroup\">\n <div\n part=\"header-row\"\n class=\"row\"\n role=\"row\"\n aria-rowindex=\"1\"\n @click=${handlers.click}\n @pointerdown=${handlers.pointerdown}\n @dblclick=${handlers.dblclick}\n >\n ${repeat(\n columns,\n (c) => c.id,\n (c) =>\n html`<div\n part=${headerParts(c, drag)}\n role=\"columnheader\"\n aria-colindex=${c.colIndex}\n aria-sort=${ariaSort(c.sorted) ?? nothing}\n data-col=${c.id}\n data-sort-col=${c.sortable ? c.id : nothing}\n data-move-col=${c.reorderable ? c.id : nothing}\n tabindex=${c.id === activeCol ? 0 : -1}\n data-align=${c.align}\n data-pin=${c.pin ?? nothing}\n style=${pinStyle(c) ?? nothing}\n >\n <slot name=${headerSlot(c.colIndex)}></slot>\n ${c.sortable ? html`<span class=\"sort-indicator\" aria-hidden=\"true\"></span>` : nothing}\n ${c.resizable ? resizeHandle(c) : nothing}\n ${\n drag?.target?.columnId === c.id\n ? html`<span\n part=\"drop-indicator\"\n data-edge=${drag.target.edge}\n aria-hidden=\"true\"\n ></span>`\n : nothing\n }\n </div>`,\n )}\n </div>\n </div>`;\n}\n\nfunction headerParts<T>(c: ColumnView<T, CellContent>, drag: HeaderDrag | undefined): string {\n let parts = c.sorted ? 'header-cell header-cell-sorted' : 'header-cell';\n if (drag?.columnId === c.id) parts += ' header-cell-dragging';\n return parts;\n}\n\n/**\n * A vertical separator with the column's width as its value. A flex column has no\n * fixed width until it is resized, so it reports none.\n */\nfunction resizeHandle<T>(c: ColumnView<T, CellContent>) {\n return html`<div\n part=\"resize-handle\"\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label=\"Resize column\"\n aria-valuenow=${c.flex === undefined ? Math.round(c.width) : nothing}\n aria-valuemin=${c.minWidth}\n aria-valuemax=${Number.isFinite(c.maxWidth) ? c.maxWidth : nothing}\n data-resize-col=${c.id}\n ></div>`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAiCA,SAAgB,aACd,SACA,UACA,WACA,MACA;CACA,OAAO,IAAI;;;;;;eAME,SAAS,MAAM;qBACT,SAAS,YAAY;kBACxB,SAAS,SAAS;;QAE5B,OACA,UACC,MAAM,EAAE,KACR,MACC,IAAI;mBACK,YAAY,GAAG,IAAI,EAAE;;4BAEZ,EAAE,SAAS;wBACf,SAAS,EAAE,MAAM,KAAK,QAAQ;uBAC/B,EAAE,GAAG;4BACA,EAAE,WAAW,EAAE,KAAK,QAAQ;4BAC5B,EAAE,cAAc,EAAE,KAAK,QAAQ;uBACpC,EAAE,OAAO,YAAY,IAAI,GAAG;yBAC1B,EAAE,MAAM;uBACV,EAAE,OAAO,QAAQ;oBACpB,SAAS,CAAC,KAAK,QAAQ;;yBAElB,WAAW,EAAE,QAAQ,EAAE;cAClC,EAAE,WAAW,IAAI,4DAA4D,QAAQ;cACrF,EAAE,YAAY,aAAa,CAAC,IAAI,QAAQ;cAExC,MAAM,QAAQ,aAAa,EAAE,KACzB,IAAI;;gCAEU,KAAK,OAAO,KAAK;;8BAG/B,QACL;iBAEP,EAAE;;;AAGR;AAEA,SAAS,YAAe,GAA+B,MAAsC;CAC3F,IAAI,QAAQ,EAAE,SAAS,mCAAmC;CAC1D,IAAI,MAAM,aAAa,EAAE,IAAI,SAAS;CACtC,OAAO;AACT;;;;;AAMA,SAAS,aAAgB,GAA+B;CACtD,OAAO,IAAI;;;;;oBAKO,EAAE,SAAS,KAAA,IAAY,KAAK,MAAM,EAAE,KAAK,IAAI,QAAQ;oBACrD,EAAE,SAAS;oBACX,OAAO,SAAS,EAAE,QAAQ,IAAI,EAAE,WAAW,QAAQ;sBACjD,EAAE,GAAG;;AAE3B"}
@@ -0,0 +1,29 @@
1
+ import { renderCell } from "./body.js";
2
+ import { pinnedSlot } from "./content.js";
3
+ import { html, nothing } from "lit";
4
+ import { repeat } from "lit/directives/repeat.js";
5
+ // src/render/pinned-rows.ts
6
+ /**
7
+ * The rows pinned to one side: a sticky row group below the header (`top`) or
8
+ * at the bottom of the view (`bottom`). They are few and never recycled, so
9
+ * they aren't pooled; content is slotted from the light DOM like body cells.
10
+ * Cells take part in the grid's roving tabindex (`active.index` is the row's
11
+ * index in its list). `firstRowIndex` is the `aria-rowindex` of the first row.
12
+ */
13
+ function renderPinnedRows(side, count, columns, firstRowIndex, active) {
14
+ if (count === 0) return nothing;
15
+ const rows = Array.from({ length: count }, (_, i) => i);
16
+ return html`<div class="pinned" data-side=${side} role="rowgroup">
17
+ ${rows.map((i) => html`<div
18
+ part=${`row row-pinned row-pinned-${side}`}
19
+ class="row"
20
+ role="row"
21
+ aria-rowindex=${firstRowIndex + i}
22
+ >
23
+ ${repeat(columns, (c) => c.id, (c) => renderCell(c, active?.index === i && c.id === active.col, pinnedSlot(side, i, c.colIndex), true))}
24
+ </div>`)}
25
+ </div>`;
26
+ }
27
+ export { renderPinnedRows };
28
+
29
+ //# sourceMappingURL=pinned-rows.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pinned-rows.js","names":[],"sources":["../../src/render/pinned-rows.ts"],"sourcesContent":["import { html, nothing } from 'lit';\nimport { repeat } from 'lit/directives/repeat.js';\nimport type { ColumnView, RowPin } from '@argus-table/core';\nimport type { CellContent } from '../types.js';\nimport { renderCell, type ActiveCell } from './body.js';\nimport { pinnedSlot } from './content.js';\n\n/**\n * The rows pinned to one side: a sticky row group below the header (`top`) or\n * at the bottom of the view (`bottom`). They are few and never recycled, so\n * they aren't pooled; content is slotted from the light DOM like body cells.\n * Cells take part in the grid's roving tabindex (`active.index` is the row's\n * index in its list). `firstRowIndex` is the `aria-rowindex` of the first row.\n */\nexport function renderPinnedRows<T>(\n side: RowPin,\n count: number,\n columns: readonly ColumnView<T, CellContent>[],\n firstRowIndex: number,\n active: ActiveCell | undefined,\n) {\n if (count === 0) return nothing;\n const rows = Array.from({ length: count }, (_, i) => i);\n return html`<div class=\"pinned\" data-side=${side} role=\"rowgroup\">\n ${rows.map(\n (i) =>\n html`<div\n part=${`row row-pinned row-pinned-${side}`}\n class=\"row\"\n role=\"row\"\n aria-rowindex=${firstRowIndex + i}\n >\n ${repeat(\n columns,\n (c) => c.id,\n (c) =>\n renderCell(\n c,\n active?.index === i && c.id === active.col,\n pinnedSlot(side, i, c.colIndex),\n true,\n ),\n )}\n </div>`,\n )}\n </div>`;\n}\n"],"mappings":";;;;;;;;;;;;AAcA,SAAgB,iBACd,MACA,OACA,SACA,eACA,QACA;CACA,IAAI,UAAU,GAAG,OAAO;CACxB,MAAM,OAAO,MAAM,KAAK,EAAE,QAAQ,MAAM,IAAI,GAAG,MAAM,CAAC;CACtD,OAAO,IAAI,iCAAiC,KAAK;MAC7C,KAAK,KACJ,MACC,IAAI;iBACK,6BAA6B,OAAO;;;0BAG3B,gBAAgB,EAAE;;YAEhC,OACA,UACC,MAAM,EAAE,KACR,MACC,WACE,GACA,QAAQ,UAAU,KAAK,EAAE,OAAO,OAAO,KACvC,WAAW,MAAM,GAAG,EAAE,QAAQ,GAC9B,IACF,CACJ,EAAE;eAER,EAAE;;AAEN"}
@@ -5,231 +5,7 @@ import { css } from "lit";
5
5
  * public token with a neutral fallback, so themes and page CSS always win.
6
6
  * `--_*` properties are private.
7
7
  */
8
- var baseStyles = css`
9
- @layer argus-base {
10
- :host {
11
- display: block;
12
- /* A table that virtualizes needs a bounded height; set your own on the element. */
13
- block-size: 400px;
14
- box-sizing: border-box;
15
- contain: content;
16
- color-scheme: light dark;
17
- color: var(--argus-fg, CanvasText);
18
- background: var(--argus-bg, Canvas);
19
- font: var(--argus-font, inherit);
20
- --_row-height: var(--argus-row-height, 40px);
21
- --_pad: var(--argus-cell-padding-inline, 12px);
22
- --_border: var(--argus-border-width, 1px) solid
23
- var(--argus-border-color, light-dark(#e5e7eb, #374151));
24
- /* 1 in LTR, -1 in RTL: mirrors the pinned-edge shadow. */
25
- --_dir: 1;
26
- }
27
- :host(:dir(rtl)) {
28
- --_dir: -1;
29
- }
30
- :host([density='compact']) {
31
- --_row-height: var(--argus-row-height, 32px);
32
- --_pad: var(--argus-cell-padding-inline, 8px);
33
- }
34
- :host([density='spacious']) {
35
- --_row-height: var(--argus-row-height, 52px);
36
- --_pad: var(--argus-cell-padding-inline, 16px);
37
- }
38
- :host([hidden]) {
39
- display: none;
40
- }
41
-
42
- [part~='table'] {
43
- position: relative;
44
- block-size: 100%;
45
- overflow: auto;
46
- overscroll-behavior: contain;
47
- /* A cell moved to by keyboard scrolls clear of the sticky header and pinned columns. */
48
- scroll-padding-block-start: var(--_row-height);
49
- scroll-padding-inline: var(--_pin-start, 0) var(--_pin-end, 0);
50
- }
51
-
52
- .row {
53
- display: grid;
54
- grid-template-columns: var(--_cols);
55
- inline-size: var(--_width);
56
- min-inline-size: 100%;
57
- block-size: var(--_row-height);
58
- box-sizing: border-box;
59
- }
60
-
61
- [part~='header'] {
62
- position: sticky;
63
- inset-block-start: 0;
64
- z-index: 2;
65
- }
66
-
67
- [part~='body'] {
68
- position: relative;
69
- contain: layout;
70
- }
71
- [part~='row'] {
72
- position: absolute;
73
- inset-block-start: 0;
74
- inset-inline-start: 0;
75
- --_row-bg: var(--argus-bg, Canvas);
76
- --_row-surface: var(--_row-bg);
77
- background: var(--_row-surface);
78
- }
79
- [part~='row-odd'] {
80
- --_row-bg: var(--argus-row-stripe-bg, var(--argus-bg, Canvas));
81
- }
82
- [part~='row']:hover {
83
- --_row-surface: var(--argus-row-hover-bg, var(--_row-bg));
84
- }
85
-
86
- [part~='cell'],
87
- [part~='header-cell'] {
88
- display: flex;
89
- align-items: center;
90
- min-inline-size: 0;
91
- box-sizing: border-box;
92
- padding-inline: var(--_pad);
93
- overflow: hidden;
94
- white-space: nowrap;
95
- border-block-end: var(--_border);
96
- }
97
- [part~='header-cell'] {
98
- background: var(--argus-header-bg, light-dark(#f9fafb, #111827));
99
- color: var(--argus-header-fg, inherit);
100
- font-weight: 600;
101
- }
102
- [part~='header-cell'] {
103
- position: relative;
104
- }
105
- [part~='resize-handle'] {
106
- position: absolute;
107
- inset-block: 0;
108
- inset-inline-end: 0;
109
- inline-size: 8px;
110
- z-index: 1;
111
- cursor: col-resize;
112
- touch-action: none;
113
- }
114
- [part~='resize-handle']::after {
115
- content: '';
116
- position: absolute;
117
- inset-block: 25%;
118
- inset-inline-end: 0;
119
- inline-size: 2px;
120
- background: var(--argus-resize-handle-color, light-dark(#9ca3af, #6b7280));
121
- opacity: 0;
122
- }
123
- [part~='header-cell']:hover [part~='resize-handle']::after,
124
- [part~='header-cell']:focus-visible [part~='resize-handle']::after,
125
- [part~='resize-handle'][data-active]::after {
126
- opacity: 1;
127
- }
128
- [data-sort-col] {
129
- cursor: pointer;
130
- user-select: none;
131
- }
132
- [part~='header-cell']:focus-visible,
133
- [part~='cell']:focus-visible {
134
- outline: var(--argus-focus-ring, 2px solid Highlight);
135
- outline-offset: -2px;
136
- }
137
- /* A small chevron after the header content; only the sorted column shows it. */
138
- .sort-indicator {
139
- flex: none;
140
- inline-size: 0.35em;
141
- block-size: 0.35em;
142
- margin-inline-start: 0.4em;
143
- /* Physical sides on purpose: the chevron is rotated, so it must not mirror in RTL. */
144
- border-right: 0.12em solid currentColor;
145
- border-bottom: 0.12em solid currentColor;
146
- visibility: hidden;
147
- }
148
- [aria-sort] .sort-indicator {
149
- visibility: visible;
150
- }
151
- [aria-sort='ascending'] .sort-indicator {
152
- transform: translateY(0.1em) rotate(-135deg);
153
- }
154
- [aria-sort='descending'] .sort-indicator {
155
- transform: translateY(-0.1em) rotate(45deg);
156
- }
157
- [data-align='end'] .sort-indicator {
158
- order: -1;
159
- margin-inline: 0 0.4em;
160
- }
161
- /*
162
- * Pinned columns: sticky inside the scroll container, with offsets from the
163
- * table model. Logical insets make RTL work without extra rules.
164
- */
165
- [data-pin] {
166
- position: sticky;
167
- z-index: 1;
168
- /* Visible so the edge shadow isn't clipped; slotted content still clips itself. */
169
- overflow: visible;
170
- }
171
- [data-pin='start'] {
172
- inset-inline-start: var(--_pin-offset);
173
- }
174
- [data-pin='end'] {
175
- inset-inline-end: var(--_pin-offset);
176
- }
177
- [part~='cell-pinned'] {
178
- background: var(--_row-surface);
179
- }
180
- /*
181
- * The edge shadow. --argus-pinned-shadow is written for a shadow falling
182
- * toward the end side; the start edge in RTL and the end edge in LTR mirror it.
183
- */
184
- [data-pin-edge]::after {
185
- content: '';
186
- position: absolute;
187
- inset: 0;
188
- pointer-events: none;
189
- box-shadow: var(
190
- --argus-pinned-shadow,
191
- 4px 0 6px -4px light-dark(rgb(0 0 0 / 0.2), rgb(0 0 0 / 0.6))
192
- );
193
- transform: scaleX(var(--_dir));
194
- }
195
- [data-pin='end'][data-pin-edge]::after {
196
- transform: scaleX(calc(-1 * var(--_dir)));
197
- }
198
-
199
- [data-align='center'] {
200
- justify-content: center;
201
- text-align: center;
202
- }
203
- [data-align='end'] {
204
- justify-content: flex-end;
205
- text-align: end;
206
- }
207
- ::slotted(*) {
208
- min-inline-size: 0;
209
- overflow: hidden;
210
- text-overflow: ellipsis;
211
- }
212
-
213
- [part~='empty'] {
214
- display: flex;
215
- align-items: center;
216
- justify-content: center;
217
- min-block-size: calc(100% - var(--_row-height));
218
- padding: var(--_pad);
219
- box-sizing: border-box;
220
- }
221
-
222
- /* Measures the resolved row height in px for the virtualizer. */
223
- .probe {
224
- position: absolute;
225
- inset-block-start: 0;
226
- inline-size: 0;
227
- block-size: var(--_row-height);
228
- visibility: hidden;
229
- pointer-events: none;
230
- }
231
- }
232
- `;
8
+ var baseStyles = css`@layer argus-base{:host{box-sizing:border-box;contain:content;color-scheme:light dark;block-size:400px;color:var(--argus-fg,CanvasText);background:var(--argus-bg,Canvas);font:var(--argus-font,inherit);border:var(--argus-outline,none);border-radius:var(--argus-radius,0);--_row-height:var(--argus-row-height,40px);--_pad:var(--argus-cell-padding-inline,12px);--_border:var(--argus-border-width,1px) solid var(--argus-border-color,light-dark(#e5e7eb,#374151));--_dir:1;display:block}:host(:dir(rtl)){--_dir:-1}:host([density=compact]){--_row-height:var(--argus-row-height,32px);--_pad:var(--argus-cell-padding-inline,8px)}:host([density=spacious]){--_row-height:var(--argus-row-height,52px);--_pad:var(--argus-cell-padding-inline,16px)}:host([hidden]){display:none}[part~=table]{overscroll-behavior:contain;block-size:100%;scroll-padding-block-start:calc((1 + var(--_pin-top-rows,0)) * var(--_row-height));scroll-padding-block-end:calc(var(--_pin-bottom-rows,0) * var(--_row-height));scroll-padding-inline:var(--_pin-start,0) var(--_pin-end,0);position:relative;overflow:auto}.row{grid-template-columns:var(--_cols);inline-size:var(--_width);min-inline-size:100%;block-size:var(--_row-height);box-sizing:border-box;display:grid}[part~=header]{z-index:2;position:sticky;inset-block-start:0}[part~=body]{contain:layout;position:relative}[part~=body]>.row{position:absolute;inset-block-start:0;inset-inline-start:0}[part~=row]{--_row-bg:var(--argus-bg,Canvas);--_row-surface:var(--_row-bg);background:var(--_row-surface)}[part~=row-odd]{--_row-bg:var(--argus-row-stripe-bg,var(--argus-bg,Canvas))}[part~=row]:hover{--_row-surface:var(--argus-row-hover-bg,var(--_row-bg))}.pinned{z-index:2;position:sticky}.pinned[data-side=top]{inset-block-start:var(--_row-height)}.pinned[data-side=bottom]{inset-block-end:0}[part~=row-pinned]{--_row-bg:var(--argus-pinned-row-bg,var(--argus-bg,Canvas))}[part~=cell],[part~=header-cell]{box-sizing:border-box;min-inline-size:0;padding-inline:var(--_pad);white-space:nowrap;border-block-end:var(--_border);align-items:center;display:flex;overflow:hidden}[part~=header-cell]{background:var(--argus-header-bg,light-dark(#f9fafb,#111827));color:var(--argus-header-fg,inherit);font-weight:600;position:relative}[part~=resize-handle]{z-index:1;cursor:col-resize;touch-action:none;inline-size:8px;position:absolute;inset-block:0;inset-inline-end:0}[part~=resize-handle]:after{content:"";background:var(--argus-resize-handle-color,light-dark(#9ca3af,#6b7280));opacity:0;inline-size:2px;position:absolute;inset-block:25%;inset-inline-end:0}[part~=header-cell]:hover [part~=resize-handle]:after,[part~=header-cell]:focus-visible [part~=resize-handle]:after,[part~=resize-handle][data-active]:after{opacity:1}[data-sort-col]{cursor:pointer;user-select:none}[data-move-col]{user-select:none}[data-dragging],[data-dragging] [part~=header-cell]{cursor:grabbing}[part~=header-cell-dragging]{opacity:.5}[part~=drop-indicator]{z-index:2;background:var(--argus-drop-indicator-color,Highlight);pointer-events:none;inline-size:2px;position:absolute;inset-block:0}[part~=drop-indicator][data-edge=start]{inset-inline-start:0}[part~=drop-indicator][data-edge=end]{inset-inline-end:0}[part~=header-cell]:focus-visible,[part~=cell]:focus-visible{outline:var(--argus-focus-ring,2px solid Highlight);outline-offset:-2px}.sort-indicator{visibility:hidden;border-bottom:.12em solid;border-right:.12em solid;flex:none;block-size:.35em;inline-size:.35em;margin-inline-start:.4em}[aria-sort] .sort-indicator{visibility:visible}[aria-sort=ascending] .sort-indicator{transform:translateY(.1em)rotate(-135deg)}[aria-sort=descending] .sort-indicator{transform:translateY(-.1em)rotate(45deg)}[data-align=end] .sort-indicator{order:-1;margin-inline:0 .4em}[data-pin]{z-index:1;position:sticky}[data-pin=start]{inset-inline-start:var(--_pin-offset)}[data-pin=end]{inset-inline-end:var(--_pin-offset)}[part~=row]>[data-pin]{position:relative;inset-inline:auto}[part~=row]>[data-pin=start]{transform:translateX(var(--_sx,0px))}[part~=row]>[data-pin=end]{transform:translateX(calc(var(--_sx,0px) - var(--_dir) * var(--_max-x,0px)))}[data-hscroll] [part~=row]>[data-pin]{position:sticky;transform:none}[data-hscroll] [part~=row]>[data-pin=start]{inset-inline-start:var(--_pin-offset)}[data-hscroll] [part~=row]>[data-pin=end]{inset-inline-end:var(--_pin-offset)}[part~=cell-pinned]{background:var(--_row-surface)}.pin-shadow{block-size:min(var(--_view-h,100%), var(--_content-h,100%));z-index:3;pointer-events:none;position:absolute;inset-block-start:0}.pin-shadow[data-side=start]{inset-inline-start:0}.pin-shadow[data-side=end]{inset-inline-end:var(--_sb-x,0px)}.pin-shadow:after{content:"";box-shadow:var(--argus-pinned-shadow,4px 0 6px -4px light-dark(#0003,#0009));transform:scaleX(var(--_dir));position:absolute;inset:0}.pin-shadow[data-side=end]:after{transform:scaleX(calc(-1 * var(--_dir)))}[data-align=center]{text-align:center;justify-content:center}[data-align=end]{text-align:end;justify-content:flex-end}::slotted(*){text-overflow:ellipsis;min-inline-size:0;overflow:hidden}[data-measure]{inline-size:max-content}[part~=skeleton]{background:var(--argus-skeleton-bg,color-mix(in srgb, currentColor 12%, transparent));border-radius:.3em;flex:none;block-size:.6em;inline-size:min(60%,8em);animation:1.6s ease-in-out infinite argus-skeleton-pulse}[part~=skeleton-error]{opacity:.5;animation:none}@keyframes argus-skeleton-pulse{50%{opacity:.45}}@media (prefers-reduced-motion:reduce){[part~=skeleton]{animation:none}}[part~=retry]{font:inherit;color:inherit;cursor:pointer}[part~=empty]{min-block-size:calc(100% - (1 + var(--_pin-top-rows,0) + var(--_pin-bottom-rows,0)) * var(--_row-height));padding:var(--_pad);box-sizing:border-box;justify-content:center;align-items:center;display:flex}.probe{inline-size:0;block-size:var(--_row-height);visibility:hidden;pointer-events:none;position:absolute;inset-block-start:0}}`;
233
9
  export { baseStyles };
234
10
 
235
11
  //# sourceMappingURL=base.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"base.js","names":[],"sources":["../../src/styles/base.ts"],"sourcesContent":["import { css } from 'lit';\n\n/**\n * Base styles. Everything sits in `@layer argus-base`, and every value reads a\n * public token with a neutral fallback, so themes and page CSS always win.\n * `--_*` properties are private.\n */\nexport const baseStyles = css`\n @layer argus-base {\n :host {\n display: block;\n /* A table that virtualizes needs a bounded height; set your own on the element. */\n block-size: 400px;\n box-sizing: border-box;\n contain: content;\n color-scheme: light dark;\n color: var(--argus-fg, CanvasText);\n background: var(--argus-bg, Canvas);\n font: var(--argus-font, inherit);\n --_row-height: var(--argus-row-height, 40px);\n --_pad: var(--argus-cell-padding-inline, 12px);\n --_border: var(--argus-border-width, 1px) solid\n var(--argus-border-color, light-dark(#e5e7eb, #374151));\n /* 1 in LTR, -1 in RTL: mirrors the pinned-edge shadow. */\n --_dir: 1;\n }\n :host(:dir(rtl)) {\n --_dir: -1;\n }\n :host([density='compact']) {\n --_row-height: var(--argus-row-height, 32px);\n --_pad: var(--argus-cell-padding-inline, 8px);\n }\n :host([density='spacious']) {\n --_row-height: var(--argus-row-height, 52px);\n --_pad: var(--argus-cell-padding-inline, 16px);\n }\n :host([hidden]) {\n display: none;\n }\n\n [part~='table'] {\n position: relative;\n block-size: 100%;\n overflow: auto;\n overscroll-behavior: contain;\n /* A cell moved to by keyboard scrolls clear of the sticky header and pinned columns. */\n scroll-padding-block-start: var(--_row-height);\n scroll-padding-inline: var(--_pin-start, 0) var(--_pin-end, 0);\n }\n\n .row {\n display: grid;\n grid-template-columns: var(--_cols);\n inline-size: var(--_width);\n min-inline-size: 100%;\n block-size: var(--_row-height);\n box-sizing: border-box;\n }\n\n [part~='header'] {\n position: sticky;\n inset-block-start: 0;\n z-index: 2;\n }\n\n [part~='body'] {\n position: relative;\n contain: layout;\n }\n [part~='row'] {\n position: absolute;\n inset-block-start: 0;\n inset-inline-start: 0;\n --_row-bg: var(--argus-bg, Canvas);\n --_row-surface: var(--_row-bg);\n background: var(--_row-surface);\n }\n [part~='row-odd'] {\n --_row-bg: var(--argus-row-stripe-bg, var(--argus-bg, Canvas));\n }\n [part~='row']:hover {\n --_row-surface: var(--argus-row-hover-bg, var(--_row-bg));\n }\n\n [part~='cell'],\n [part~='header-cell'] {\n display: flex;\n align-items: center;\n min-inline-size: 0;\n box-sizing: border-box;\n padding-inline: var(--_pad);\n overflow: hidden;\n white-space: nowrap;\n border-block-end: var(--_border);\n }\n [part~='header-cell'] {\n background: var(--argus-header-bg, light-dark(#f9fafb, #111827));\n color: var(--argus-header-fg, inherit);\n font-weight: 600;\n }\n [part~='header-cell'] {\n position: relative;\n }\n [part~='resize-handle'] {\n position: absolute;\n inset-block: 0;\n inset-inline-end: 0;\n inline-size: 8px;\n z-index: 1;\n cursor: col-resize;\n touch-action: none;\n }\n [part~='resize-handle']::after {\n content: '';\n position: absolute;\n inset-block: 25%;\n inset-inline-end: 0;\n inline-size: 2px;\n background: var(--argus-resize-handle-color, light-dark(#9ca3af, #6b7280));\n opacity: 0;\n }\n [part~='header-cell']:hover [part~='resize-handle']::after,\n [part~='header-cell']:focus-visible [part~='resize-handle']::after,\n [part~='resize-handle'][data-active]::after {\n opacity: 1;\n }\n [data-sort-col] {\n cursor: pointer;\n user-select: none;\n }\n [part~='header-cell']:focus-visible,\n [part~='cell']:focus-visible {\n outline: var(--argus-focus-ring, 2px solid Highlight);\n outline-offset: -2px;\n }\n /* A small chevron after the header content; only the sorted column shows it. */\n .sort-indicator {\n flex: none;\n inline-size: 0.35em;\n block-size: 0.35em;\n margin-inline-start: 0.4em;\n /* Physical sides on purpose: the chevron is rotated, so it must not mirror in RTL. */\n border-right: 0.12em solid currentColor;\n border-bottom: 0.12em solid currentColor;\n visibility: hidden;\n }\n [aria-sort] .sort-indicator {\n visibility: visible;\n }\n [aria-sort='ascending'] .sort-indicator {\n transform: translateY(0.1em) rotate(-135deg);\n }\n [aria-sort='descending'] .sort-indicator {\n transform: translateY(-0.1em) rotate(45deg);\n }\n [data-align='end'] .sort-indicator {\n order: -1;\n margin-inline: 0 0.4em;\n }\n /*\n * Pinned columns: sticky inside the scroll container, with offsets from the\n * table model. Logical insets make RTL work without extra rules.\n */\n [data-pin] {\n position: sticky;\n z-index: 1;\n /* Visible so the edge shadow isn't clipped; slotted content still clips itself. */\n overflow: visible;\n }\n [data-pin='start'] {\n inset-inline-start: var(--_pin-offset);\n }\n [data-pin='end'] {\n inset-inline-end: var(--_pin-offset);\n }\n [part~='cell-pinned'] {\n background: var(--_row-surface);\n }\n /*\n * The edge shadow. --argus-pinned-shadow is written for a shadow falling\n * toward the end side; the start edge in RTL and the end edge in LTR mirror it.\n */\n [data-pin-edge]::after {\n content: '';\n position: absolute;\n inset: 0;\n pointer-events: none;\n box-shadow: var(\n --argus-pinned-shadow,\n 4px 0 6px -4px light-dark(rgb(0 0 0 / 0.2), rgb(0 0 0 / 0.6))\n );\n transform: scaleX(var(--_dir));\n }\n [data-pin='end'][data-pin-edge]::after {\n transform: scaleX(calc(-1 * var(--_dir)));\n }\n\n [data-align='center'] {\n justify-content: center;\n text-align: center;\n }\n [data-align='end'] {\n justify-content: flex-end;\n text-align: end;\n }\n ::slotted(*) {\n min-inline-size: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n [part~='empty'] {\n display: flex;\n align-items: center;\n justify-content: center;\n min-block-size: calc(100% - var(--_row-height));\n padding: var(--_pad);\n box-sizing: border-box;\n }\n\n /* Measures the resolved row height in px for the virtualizer. */\n .probe {\n position: absolute;\n inset-block-start: 0;\n inline-size: 0;\n block-size: var(--_row-height);\n visibility: hidden;\n pointer-events: none;\n }\n }\n`;\n"],"mappings":";;;;;;;AAOA,IAAa,aAAa,GAAG"}
1
+ {"version":3,"file":"base.js","names":[],"sources":["../../src/styles/base.ts"],"sourcesContent":["import { css } from 'lit';\n\n/**\n * Base styles. Everything sits in `@layer argus-base`, and every value reads a\n * public token with a neutral fallback, so themes and page CSS always win.\n * `--_*` properties are private.\n */\nexport const baseStyles = css`\n @layer argus-base {\n :host {\n display: block;\n /* A table that virtualizes needs a bounded height; set your own on the element. */\n block-size: 400px;\n box-sizing: border-box;\n contain: content;\n color-scheme: light dark;\n color: var(--argus-fg, CanvasText);\n background: var(--argus-bg, Canvas);\n font: var(--argus-font, inherit);\n /* contain: content clips the header, rows and pinned shadows to the rounded corners. */\n border: var(--argus-outline, none);\n border-radius: var(--argus-radius, 0);\n --_row-height: var(--argus-row-height, 40px);\n --_pad: var(--argus-cell-padding-inline, 12px);\n --_border: var(--argus-border-width, 1px) solid\n var(--argus-border-color, light-dark(#e5e7eb, #374151));\n /* 1 in LTR, -1 in RTL: mirrors the pinned-edge shadow. */\n --_dir: 1;\n }\n :host(:dir(rtl)) {\n --_dir: -1;\n }\n :host([density='compact']) {\n --_row-height: var(--argus-row-height, 32px);\n --_pad: var(--argus-cell-padding-inline, 8px);\n }\n :host([density='spacious']) {\n --_row-height: var(--argus-row-height, 52px);\n --_pad: var(--argus-cell-padding-inline, 16px);\n }\n :host([hidden]) {\n display: none;\n }\n\n [part~='table'] {\n position: relative;\n block-size: 100%;\n overflow: auto;\n overscroll-behavior: contain;\n /*\n * A cell moved to by keyboard scrolls clear of the sticky header, the pinned\n * rows and the pinned columns.\n */\n scroll-padding-block-start: calc((1 + var(--_pin-top-rows, 0)) * var(--_row-height));\n scroll-padding-block-end: calc(var(--_pin-bottom-rows, 0) * var(--_row-height));\n scroll-padding-inline: var(--_pin-start, 0) var(--_pin-end, 0);\n }\n\n .row {\n display: grid;\n grid-template-columns: var(--_cols);\n inline-size: var(--_width);\n min-inline-size: 100%;\n block-size: var(--_row-height);\n box-sizing: border-box;\n }\n\n [part~='header'] {\n position: sticky;\n inset-block-start: 0;\n z-index: 2;\n }\n\n [part~='body'] {\n position: relative;\n contain: layout;\n }\n [part~='body'] > .row {\n position: absolute;\n inset-block-start: 0;\n inset-inline-start: 0;\n }\n [part~='row'] {\n --_row-bg: var(--argus-bg, Canvas);\n --_row-surface: var(--_row-bg);\n background: var(--_row-surface);\n }\n [part~='row-odd'] {\n --_row-bg: var(--argus-row-stripe-bg, var(--argus-bg, Canvas));\n }\n [part~='row']:hover {\n --_row-surface: var(--argus-row-hover-bg, var(--_row-bg));\n }\n\n /*\n * Pinned rows: a sticky row group below the header and one at the bottom of\n * the view. The header is one row tall. They keep their place in the flow,\n * so body row i still starts i rows below the top group.\n */\n .pinned {\n position: sticky;\n z-index: 2;\n }\n .pinned[data-side='top'] {\n inset-block-start: var(--_row-height);\n }\n .pinned[data-side='bottom'] {\n inset-block-end: 0;\n }\n [part~='row-pinned'] {\n --_row-bg: var(--argus-pinned-row-bg, var(--argus-bg, Canvas));\n }\n\n [part~='cell'],\n [part~='header-cell'] {\n display: flex;\n align-items: center;\n min-inline-size: 0;\n box-sizing: border-box;\n padding-inline: var(--_pad);\n overflow: hidden;\n white-space: nowrap;\n border-block-end: var(--_border);\n }\n [part~='header-cell'] {\n background: var(--argus-header-bg, light-dark(#f9fafb, #111827));\n color: var(--argus-header-fg, inherit);\n font-weight: 600;\n }\n [part~='header-cell'] {\n position: relative;\n }\n [part~='resize-handle'] {\n position: absolute;\n inset-block: 0;\n inset-inline-end: 0;\n inline-size: 8px;\n z-index: 1;\n cursor: col-resize;\n touch-action: none;\n }\n [part~='resize-handle']::after {\n content: '';\n position: absolute;\n inset-block: 25%;\n inset-inline-end: 0;\n inline-size: 2px;\n background: var(--argus-resize-handle-color, light-dark(#9ca3af, #6b7280));\n opacity: 0;\n }\n [part~='header-cell']:hover [part~='resize-handle']::after,\n [part~='header-cell']:focus-visible [part~='resize-handle']::after,\n [part~='resize-handle'][data-active]::after {\n opacity: 1;\n }\n [data-sort-col] {\n cursor: pointer;\n user-select: none;\n }\n /* A drag on a reorderable header moves the column, so its text isn't selectable. */\n [data-move-col] {\n user-select: none;\n }\n [data-dragging],\n [data-dragging] [part~='header-cell'] {\n cursor: grabbing;\n }\n [part~='header-cell-dragging'] {\n opacity: 0.5;\n }\n /* The line where a dragged column would land, on the target header's logical edge. */\n [part~='drop-indicator'] {\n position: absolute;\n inset-block: 0;\n inline-size: 2px;\n z-index: 2;\n background: var(--argus-drop-indicator-color, Highlight);\n pointer-events: none;\n }\n [part~='drop-indicator'][data-edge='start'] {\n inset-inline-start: 0;\n }\n [part~='drop-indicator'][data-edge='end'] {\n inset-inline-end: 0;\n }\n [part~='header-cell']:focus-visible,\n [part~='cell']:focus-visible {\n outline: var(--argus-focus-ring, 2px solid Highlight);\n outline-offset: -2px;\n }\n /* A small chevron after the header content; only the sorted column shows it. */\n .sort-indicator {\n flex: none;\n inline-size: 0.35em;\n block-size: 0.35em;\n margin-inline-start: 0.4em;\n /* Physical sides on purpose: the chevron is rotated, so it must not mirror in RTL. */\n border-right: 0.12em solid currentColor;\n border-bottom: 0.12em solid currentColor;\n visibility: hidden;\n }\n [aria-sort] .sort-indicator {\n visibility: visible;\n }\n [aria-sort='ascending'] .sort-indicator {\n transform: translateY(0.1em) rotate(-135deg);\n }\n [aria-sort='descending'] .sort-indicator {\n transform: translateY(-0.1em) rotate(45deg);\n }\n [data-align='end'] .sort-indicator {\n order: -1;\n margin-inline: 0 0.4em;\n }\n /*\n * Pinned columns: sticky inside the scroll container, with offsets from the\n * table model. Logical insets make RTL work without extra rules.\n */\n [data-pin] {\n position: sticky;\n z-index: 1;\n }\n [data-pin='start'] {\n inset-inline-start: var(--_pin-offset);\n }\n [data-pin='end'] {\n inset-inline-end: var(--_pin-offset);\n }\n /*\n * Pinned body cells are sticky only while the table scrolls sideways\n * (data-hscroll on the scroller). At rest they are plain cells shifted by the\n * horizontal scroll offset, which vertical scrolling never changes: a sticky\n * cell per row is a compositing layer each, and Safari stutters moving them\n * (PinScrollController). Both place the cells at the same spot.\n */\n [part~='row'] > [data-pin] {\n position: relative;\n inset-inline: auto;\n }\n [part~='row'] > [data-pin='start'] {\n transform: translateX(var(--_sx, 0px));\n }\n [part~='row'] > [data-pin='end'] {\n transform: translateX(calc(var(--_sx, 0px) - var(--_dir) * var(--_max-x, 0px)));\n }\n [data-hscroll] [part~='row'] > [data-pin] {\n position: sticky;\n transform: none;\n }\n [data-hscroll] [part~='row'] > [data-pin='start'] {\n inset-inline-start: var(--_pin-offset);\n }\n [data-hscroll] [part~='row'] > [data-pin='end'] {\n inset-inline-end: var(--_pin-offset);\n }\n [part~='cell-pinned'] {\n background: var(--_row-surface);\n }\n /*\n * The edge shadow, once per pinned side (.pin-shadow, beside the scroller).\n * --argus-pinned-shadow is written for a shadow falling toward the end side;\n * the start edge in RTL and the end edge in LTR mirror it.\n */\n .pin-shadow {\n position: absolute;\n inset-block-start: 0;\n block-size: min(var(--_view-h, 100%), var(--_content-h, 100%));\n z-index: 3;\n pointer-events: none;\n }\n .pin-shadow[data-side='start'] {\n inset-inline-start: 0;\n }\n .pin-shadow[data-side='end'] {\n inset-inline-end: var(--_sb-x, 0px);\n }\n .pin-shadow::after {\n content: '';\n position: absolute;\n inset: 0;\n box-shadow: var(\n --argus-pinned-shadow,\n 4px 0 6px -4px light-dark(rgb(0 0 0 / 0.2), rgb(0 0 0 / 0.6))\n );\n transform: scaleX(var(--_dir));\n }\n .pin-shadow[data-side='end']::after {\n transform: scaleX(calc(-1 * var(--_dir)));\n }\n\n [data-align='center'] {\n justify-content: center;\n text-align: center;\n }\n [data-align='end'] {\n justify-content: flex-end;\n text-align: end;\n }\n ::slotted(*) {\n min-inline-size: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n /* Fitting a column to its content: set for one synchronous measurement, never painted. */\n [data-measure] {\n inline-size: max-content;\n }\n\n /* Rows of a data source that haven't loaded: a bar per cell, pulsing gently. */\n [part~='skeleton'] {\n flex: none;\n inline-size: min(60%, 8em);\n block-size: 0.6em;\n border-radius: 0.3em;\n background: var(--argus-skeleton-bg, color-mix(in srgb, currentColor 12%, transparent));\n animation: argus-skeleton-pulse 1.6s ease-in-out infinite;\n }\n [part~='skeleton-error'] {\n animation: none;\n opacity: 0.5;\n }\n @keyframes argus-skeleton-pulse {\n 50% {\n opacity: 0.45;\n }\n }\n @media (prefers-reduced-motion: reduce) {\n [part~='skeleton'] {\n animation: none;\n }\n }\n [part~='retry'] {\n font: inherit;\n color: inherit;\n cursor: pointer;\n }\n\n [part~='empty'] {\n display: flex;\n align-items: center;\n justify-content: center;\n min-block-size: calc(\n 100% - (1 + var(--_pin-top-rows, 0) + var(--_pin-bottom-rows, 0)) * var(--_row-height)\n );\n padding: var(--_pad);\n box-sizing: border-box;\n }\n\n /* Measures the resolved row height in px for the virtualizer. */\n .probe {\n position: absolute;\n inset-block-start: 0;\n inline-size: 0;\n block-size: var(--_row-height);\n visibility: hidden;\n pointer-events: none;\n }\n }\n`;\n"],"mappings":";;;;;;;AAOA,IAAa,aAAa,GAAG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argus-table",
3
- "version": "0.0.1",
3
+ "version": "0.2.0",
4
4
  "description": "A framework-agnostic data table web component built with Lit and TanStack.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,6 +27,7 @@
27
27
  ],
28
28
  "sideEffects": [
29
29
  "./dist/index.js",
30
+ "./dist/argus-column.js",
30
31
  "./dist/argus-table.js",
31
32
  "./dist/cdn/argus-table.js"
32
33
  ],
@@ -40,6 +41,7 @@
40
41
  "@custom-elements-manifest/analyzer": "^0.11.0",
41
42
  "@size-limit/file": "^14.3.0",
42
43
  "@vitest/browser-playwright": "^5.1.0",
44
+ "lightningcss": "1.33.0",
43
45
  "playwright": "1.63.0",
44
46
  "publint": "^0.3.25",
45
47
  "rollup": "~4.64.12",