@aceshooting/lyra-ui 1.0.1 → 1.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 (150) hide show
  1. package/README.md +9 -10
  2. package/custom-elements.json +15118 -8342
  3. package/dist/components/chart/chart.d.ts +57 -1
  4. package/dist/components/chart/chart.js +202 -12
  5. package/dist/components/chart/chart.stories.d.ts +32 -0
  6. package/dist/components/chart/chart.stories.js +141 -0
  7. package/dist/components/chart/chart.styles.js +16 -0
  8. package/dist/components/chart/histogram.d.ts +2 -0
  9. package/dist/components/chart/histogram.js +18 -2
  10. package/dist/components/chart/line-chart.stories.d.ts +17 -0
  11. package/dist/components/chart/line-chart.stories.js +75 -0
  12. package/dist/components/chart/lite-chart.d.ts +83 -0
  13. package/dist/components/chart/lite-chart.js +424 -0
  14. package/dist/components/chart/lite-chart.stories.d.ts +14 -0
  15. package/dist/components/chart/lite-chart.stories.js +114 -0
  16. package/dist/components/chart/lite-chart.styles.d.ts +1 -0
  17. package/dist/components/chart/lite-chart.styles.js +83 -0
  18. package/dist/components/combobox/combobox.d.ts +9 -0
  19. package/dist/components/combobox/combobox.js +76 -13
  20. package/dist/components/combobox/combobox.stories.d.ts +18 -0
  21. package/dist/components/combobox/combobox.stories.js +75 -0
  22. package/dist/components/date-picker/calendar-core.js +13 -2
  23. package/dist/components/date-picker/date-input.d.ts +9 -1
  24. package/dist/components/date-picker/date-input.js +55 -7
  25. package/dist/components/date-picker/date-input.stories.d.ts +5 -0
  26. package/dist/components/date-picker/date-input.stories.js +36 -0
  27. package/dist/components/date-picker/date-picker.d.ts +11 -3
  28. package/dist/components/date-picker/date-picker.js +87 -37
  29. package/dist/components/date-picker/date-picker.stories.d.ts +4 -0
  30. package/dist/components/date-picker/date-picker.stories.js +12 -0
  31. package/dist/components/date-picker/date-picker.styles.js +1 -1
  32. package/dist/components/empty/empty.d.ts +12 -1
  33. package/dist/components/empty/empty.js +20 -3
  34. package/dist/components/empty/empty.stories.d.ts +2 -0
  35. package/dist/components/empty/empty.stories.js +20 -0
  36. package/dist/components/empty/empty.styles.js +11 -0
  37. package/dist/components/export-button/csv.js +6 -2
  38. package/dist/components/export-button/export-button.d.ts +15 -1
  39. package/dist/components/export-button/export-button.js +89 -12
  40. package/dist/components/export-button/export-button.stories.d.ts +1 -0
  41. package/dist/components/export-button/export-button.stories.js +5 -0
  42. package/dist/components/export-button/export-button.styles.js +11 -3
  43. package/dist/components/file-input/accept.d.ts +8 -2
  44. package/dist/components/file-input/accept.js +9 -3
  45. package/dist/components/file-input/file-input.d.ts +5 -2
  46. package/dist/components/file-input/file-input.js +23 -8
  47. package/dist/components/file-input/file-input.stories.d.ts +2 -0
  48. package/dist/components/file-input/file-input.stories.js +9 -0
  49. package/dist/components/flag/flag.d.ts +69 -3
  50. package/dist/components/flag/flag.js +107 -30
  51. package/dist/components/flag/flag.stories.d.ts +1 -0
  52. package/dist/components/flag/flag.stories.js +14 -0
  53. package/dist/components/flag/flag.styles.js +1 -1
  54. package/dist/components/flag/language-map.d.ts +11 -0
  55. package/dist/components/flag/language-map.js +12 -1
  56. package/dist/components/gauge/gauge.d.ts +2 -0
  57. package/dist/components/gauge/gauge.js +19 -5
  58. package/dist/components/gauge/gauge.stories.d.ts +2 -0
  59. package/dist/components/gauge/gauge.stories.js +6 -0
  60. package/dist/components/graph/graph-loader.d.ts +28 -0
  61. package/dist/components/graph/graph-loader.js +44 -0
  62. package/dist/components/graph/graph.d.ts +41 -15
  63. package/dist/components/graph/graph.js +146 -71
  64. package/dist/components/graph/graph.stories.d.ts +3 -0
  65. package/dist/components/graph/graph.stories.js +38 -0
  66. package/dist/components/graph/graph.styles.js +6 -1
  67. package/dist/components/heatmap/calendar-grid.d.ts +7 -0
  68. package/dist/components/heatmap/calendar-grid.js +27 -10
  69. package/dist/components/heatmap/heatmap.d.ts +149 -12
  70. package/dist/components/heatmap/heatmap.js +445 -28
  71. package/dist/components/heatmap/heatmap.stories.d.ts +22 -0
  72. package/dist/components/heatmap/heatmap.stories.js +155 -0
  73. package/dist/components/heatmap/heatmap.styles.js +60 -2
  74. package/dist/components/map/map.d.ts +17 -1
  75. package/dist/components/map/map.js +68 -21
  76. package/dist/components/map/map.stories.d.ts +10 -0
  77. package/dist/components/map/map.stories.js +107 -12
  78. package/dist/components/map/map.styles.js +2 -2
  79. package/dist/components/playback/playback.d.ts +6 -2
  80. package/dist/components/playback/playback.js +36 -12
  81. package/dist/components/playback/playback.stories.d.ts +2 -0
  82. package/dist/components/playback/playback.stories.js +6 -0
  83. package/dist/components/playback/playback.styles.js +10 -2
  84. package/dist/components/select/select.d.ts +125 -0
  85. package/dist/components/select/select.js +480 -0
  86. package/dist/components/select/select.stories.d.ts +21 -0
  87. package/dist/components/select/select.stories.js +91 -0
  88. package/dist/components/select/select.styles.d.ts +1 -0
  89. package/dist/components/select/select.styles.js +212 -0
  90. package/dist/components/skeleton/skeleton.js +4 -7
  91. package/dist/components/sparkline/sparkline.d.ts +1 -1
  92. package/dist/components/sparkline/sparkline.js +40 -13
  93. package/dist/components/sparkline/sparkline.stories.d.ts +4 -0
  94. package/dist/components/sparkline/sparkline.stories.js +22 -0
  95. package/dist/components/split/split.d.ts +24 -3
  96. package/dist/components/split/split.js +155 -31
  97. package/dist/components/split/split.stories.d.ts +2 -0
  98. package/dist/components/split/split.stories.js +20 -0
  99. package/dist/components/stat/stat.d.ts +43 -1
  100. package/dist/components/stat/stat.js +97 -4
  101. package/dist/components/stat/stat.stories.d.ts +4 -0
  102. package/dist/components/stat/stat.stories.js +66 -1
  103. package/dist/components/stat/stat.styles.js +64 -0
  104. package/dist/components/table/table.d.ts +64 -2
  105. package/dist/components/table/table.js +213 -17
  106. package/dist/components/table/table.stories.d.ts +6 -0
  107. package/dist/components/table/table.stories.js +45 -0
  108. package/dist/components/table/table.styles.js +51 -4
  109. package/dist/components/time-range/time-range.d.ts +34 -5
  110. package/dist/components/time-range/time-range.js +217 -36
  111. package/dist/components/time-range/time-range.stories.d.ts +2 -0
  112. package/dist/components/time-range/time-range.stories.js +13 -0
  113. package/dist/components/time-range/time-range.styles.js +44 -2
  114. package/dist/components/toast/toast-item.d.ts +14 -2
  115. package/dist/components/toast/toast-item.js +134 -17
  116. package/dist/components/toast/toast-item.styles.js +28 -2
  117. package/dist/components/toast/toast.stories.d.ts +3 -0
  118. package/dist/components/toast/toast.stories.js +41 -1
  119. package/dist/components/toast/toast.styles.js +11 -11
  120. package/dist/components/toast/toaster.d.ts +1 -1
  121. package/dist/components/toast/toaster.js +13 -6
  122. package/dist/components/tree/tree-node.d.ts +13 -7
  123. package/dist/components/tree/tree-node.js +24 -10
  124. package/dist/components/tree/tree.d.ts +64 -15
  125. package/dist/components/tree/tree.js +113 -22
  126. package/dist/components/tree/tree.stories.d.ts +4 -0
  127. package/dist/components/tree/tree.stories.js +31 -1
  128. package/dist/components/tree/update-cascade.d.ts +13 -0
  129. package/dist/components/tree/update-cascade.js +13 -0
  130. package/dist/components/widget/widget.d.ts +25 -1
  131. package/dist/components/widget/widget.js +164 -6
  132. package/dist/components/widget/widget.stories.d.ts +3 -0
  133. package/dist/components/widget/widget.stories.js +43 -0
  134. package/dist/components/widget/widget.styles.js +20 -3
  135. package/dist/components/word-cloud/word-cloud-layout.d.ts +63 -0
  136. package/dist/components/word-cloud/word-cloud-layout.js +145 -0
  137. package/dist/components/word-cloud/word-cloud.d.ts +65 -0
  138. package/dist/components/word-cloud/word-cloud.js +255 -0
  139. package/dist/components/word-cloud/word-cloud.stories.d.ts +10 -0
  140. package/dist/components/word-cloud/word-cloud.stories.js +55 -0
  141. package/dist/components/word-cloud/word-cloud.styles.d.ts +1 -0
  142. package/dist/components/word-cloud/word-cloud.styles.js +55 -0
  143. package/dist/internal/a11y.js +2 -1
  144. package/dist/internal/rtl.d.ts +7 -0
  145. package/dist/internal/rtl.js +9 -0
  146. package/dist/lyra.d.ts +9 -1
  147. package/dist/lyra.js +6 -0
  148. package/llms-full.txt +962 -276
  149. package/llms.txt +12 -9
  150. package/package.json +6 -4
@@ -76,6 +76,21 @@ export const styles = css `
76
76
  :host([variant='danger']) [part='value'] {
77
77
  color: var(--lyra-color-danger);
78
78
  }
79
+ [part='spark'] {
80
+ /* Consumers compose their own <lyra-sparkline slot="spark">; this part
81
+ only needs to reserve width for it. */
82
+ display: block;
83
+ }
84
+ [part='spark'][hidden] {
85
+ display: none;
86
+ }
87
+ [part='sub'] {
88
+ font-size: 0.8125rem;
89
+ font-weight: 600;
90
+ }
91
+ [part='sub'][hidden] {
92
+ display: none;
93
+ }
79
94
  [part='caption'] {
80
95
  font-size: 0.8125rem;
81
96
  color: var(--lyra-color-text-quiet);
@@ -83,4 +98,53 @@ export const styles = css `
83
98
  [part='caption'][hidden] {
84
99
  display: none;
85
100
  }
101
+ [part='rows'] {
102
+ display: flex;
103
+ flex-direction: column;
104
+ /* Slightly tighter than the card's own --lyra-space-xs gap so the
105
+ breakdown list reads as one nested group rather than siblings of
106
+ equal weight with label/value/caption. */
107
+ gap: 0.125rem;
108
+ }
109
+ [part='rows'][hidden] {
110
+ display: none;
111
+ }
112
+ [part='row'] {
113
+ display: flex;
114
+ align-items: baseline;
115
+ justify-content: space-between;
116
+ gap: var(--lyra-space-xs);
117
+ font-size: 0.8125rem;
118
+ }
119
+ [part='row-label'] {
120
+ color: var(--lyra-color-text-quiet);
121
+ }
122
+ [part='row-value'] {
123
+ font-family: ui-monospace, SFMono-Regular, monospace;
124
+ font-weight: 600;
125
+ }
126
+ /* Orthogonal to the status variant: an accent edge that marks a stat as
127
+ visually emphasized (e.g. the "headline" stat in a group) regardless of
128
+ status color. */
129
+ :host([emphasis]) [part='base'] {
130
+ border-inline-start: 3px solid var(--lyra-color-brand);
131
+ }
132
+ /* Status semantics win over visual emphasis: only tint the value with the
133
+ brand color when there's no status variant already claiming it. */
134
+ :host([emphasis][variant='neutral']) [part='value'] {
135
+ color: var(--lyra-color-brand);
136
+ }
137
+ :host([prose]) [part='value'] {
138
+ font-size: 0.9375rem;
139
+ font-weight: 400;
140
+ font-family: inherit;
141
+ color: var(--lyra-color-text-quiet);
142
+ }
143
+ :host([prose]) [part='unit'] {
144
+ display: none;
145
+ }
146
+ :host([compact]) [part='base'] {
147
+ padding: var(--lyra-space-s);
148
+ gap: 0.125rem;
149
+ }
86
150
  `;
@@ -6,6 +6,15 @@ export interface TableColumn<T> {
6
6
  label: string;
7
7
  sortable?: boolean;
8
8
  align?: 'start' | 'end';
9
+ /** Responsive priority — `undefined` (the default) means "always visible".
10
+ * `'low'` columns hide first (narrowest container), `'medium'` next, as
11
+ * `[part='base']`'s container-query width shrinks; both can be forced back
12
+ * on via `[part='reveal-columns-button']`. */
13
+ priority?: 'medium' | 'low';
14
+ /** Pins this column's header/cell to the inline-start edge with
15
+ * `position: sticky` so it stays visible while the table scrolls
16
+ * horizontally. */
17
+ sticky?: boolean;
9
18
  cell: (row: T) => unknown;
10
19
  }
11
20
  /**
@@ -14,13 +23,31 @@ export interface TableColumn<T> {
14
23
  * Header/row activation is delegated: one `click` and one `keydown`
15
24
  * listener on `<table>` resolve the target via `closest('[data-col-key]'
16
25
  * | '[data-row-key]')` and a key→object lookup map, instead of allocating
17
- * fresh per-column/per-row closures on every render.
26
+ * fresh per-column/per-row closures on every render. Both listeners guard
27
+ * against nested interactive `cell()` content first (see
28
+ * `INTERACTIVE_SELECTOR`) so a button/link/input inside a cell owns its own
29
+ * activation instead of triggering `lyra-row-click`.
30
+ *
31
+ * Keyboard focus follows a roving-tabindex pattern (one `tabindex="0"` stop
32
+ * among the header cells, one among the body rows — see `focusedColKey()` /
33
+ * `focusedRowKey()`), matching this repo's other `role="grid"`/composite
34
+ * widgets. Left/Right/Home/End move within the header row; Up/Down/Home/End
35
+ * move within the body; Down from the header enters the body's roving stop,
36
+ * and Up from the body's first row returns to the header's roving stop.
37
+ * Enter/Space still only sort/activate (see `activateColumn()` /
38
+ * `activateRow()`).
39
+ *
40
+ * `columns[].priority` ('medium' | 'low') hides that column under
41
+ * `[part='base']`'s `@container` breakpoints; `[part='reveal-columns-button']`
42
+ * (rendered whenever any column has a priority) forces them all back into
43
+ * view. `columns[].sticky` pins a column's header/cells to the inline-start
44
+ * edge while the table scrolls horizontally.
18
45
  *
19
46
  * @customElement lyra-table
20
47
  * @event lyra-sort - A sortable header was activated. `detail: { key }`.
21
48
  * @event lyra-row-click - A row was activated. `detail: { row }`.
22
49
  * @event lyra-load-more - The "load more" control was activated.
23
- * @csspart base, table, head, header-cell, row, cell, more-button, sort-icon
50
+ * @csspart base, table, head, header-cell, row, cell, more-button, sort-icon, reveal-columns-button
24
51
  */
25
52
  export declare class LyraTable<T = unknown> extends LyraElement {
26
53
  static styles: import("lit").CSSResultGroup[];
@@ -28,6 +55,13 @@ export declare class LyraTable<T = unknown> extends LyraElement {
28
55
  rows: T[];
29
56
  sortKey: string;
30
57
  sortDir: 'asc' | 'desc';
58
+ /** Derives each row's stable identity for `repeat()`'s DOM-reconciliation
59
+ * key and the delegated click/keydown row lookup (`rowsByKey`,
60
+ * `data-row-key`). When omitted, `keyOf()` falls back to the row's index
61
+ * in `rows`, which is only a safe identity while `rows` never reorders —
62
+ * provide `rowKey` whenever `rows` can be sorted, filtered, or otherwise
63
+ * re-ordered across renders, or row identity (selection, focus, click
64
+ * targets) can silently attach to the wrong row. */
31
65
  rowKey?: (row: T) => string | number;
32
66
  selectedKey: string | number | null;
33
67
  hasMore: boolean;
@@ -36,14 +70,42 @@ export declare class LyraTable<T = unknown> extends LyraElement {
36
70
  emptyDescription: string;
37
71
  noColumnsHeading: string;
38
72
  noColumnsDescription: string;
73
+ revealColumnsLabel: string;
74
+ hideColumnsLabel: string;
75
+ /** Forces `priority`-hidden columns back into view, overriding the
76
+ * `@container` hide rules in table.styles.ts. Toggled by
77
+ * `[part='reveal-columns-button']`. */
78
+ private showAllColumns;
79
+ /** Roving-tabindex position among header cells; `null` until a header is
80
+ * clicked/navigated to, at which point `focusedColKey()` falls back to
81
+ * the first column. */
82
+ private activeColKey;
83
+ /** Roving-tabindex position among body rows; `null` until a row is
84
+ * clicked/navigated to, at which point `focusedRowKey()` falls back to
85
+ * `selectedKey` (if it matches a row) or the first row. */
86
+ private activeRowKey;
39
87
  private rowsByKey;
40
88
  private columnsByKey;
41
89
  private keyOf;
90
+ /** The header cell that currently owns `tabindex="0"`. */
91
+ private focusedColKey;
92
+ /** The body row that currently owns `tabindex="0"`. */
93
+ private focusedRowKey;
42
94
  protected willUpdate(changed: PropertyValues): void;
43
95
  private activateColumn;
44
96
  private activateRow;
97
+ /** Header cells currently in the tab sequence — excludes columns hidden by
98
+ * a `priority`-driven `@container` rule (table.styles.ts), so Left/Right/
99
+ * Home/End never strand the roving tab stop on a `display: none` cell
100
+ * that `.focus()` would silently no-op on. */
101
+ private visibleHeaders;
102
+ private focusHeader;
103
+ private focusRow;
104
+ private toggleColumns;
45
105
  private onTableClick;
46
106
  private onTableKeyDown;
107
+ private onHeaderKeyDown;
108
+ private onRowKeyDown;
47
109
  render(): TemplateResult;
48
110
  }
49
111
  declare global {
@@ -5,26 +5,49 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  import { html, nothing } from 'lit';
8
- import { property } from 'lit/decorators.js';
8
+ import { property, state } from 'lit/decorators.js';
9
9
  import { repeat } from 'lit/directives/repeat.js';
10
10
  import { LyraElement } from '../../internal/lyra-element.js';
11
11
  import { defineElement } from '../../internal/prefix.js';
12
12
  import { styles } from './table.styles.js';
13
13
  import { chevronIcon } from '../../internal/icons.js';
14
14
  import '../empty/empty.js';
15
+ /** Interactive elements a nested `cell()` template may render (e.g. an
16
+ * actions-column button). Clicks/keydowns landing on one of these — or
17
+ * bubbling up through one — must not be re-interpreted as row/column
18
+ * activation by the table's own delegated listeners. */
19
+ const INTERACTIVE_SELECTOR = 'button, a[href], input, select, textarea, [role="button"]';
15
20
  /**
16
21
  * `<lyra-table>` — a presentational, sort/select-aware data table.
17
22
  *
18
23
  * Header/row activation is delegated: one `click` and one `keydown`
19
24
  * listener on `<table>` resolve the target via `closest('[data-col-key]'
20
25
  * | '[data-row-key]')` and a key→object lookup map, instead of allocating
21
- * fresh per-column/per-row closures on every render.
26
+ * fresh per-column/per-row closures on every render. Both listeners guard
27
+ * against nested interactive `cell()` content first (see
28
+ * `INTERACTIVE_SELECTOR`) so a button/link/input inside a cell owns its own
29
+ * activation instead of triggering `lyra-row-click`.
30
+ *
31
+ * Keyboard focus follows a roving-tabindex pattern (one `tabindex="0"` stop
32
+ * among the header cells, one among the body rows — see `focusedColKey()` /
33
+ * `focusedRowKey()`), matching this repo's other `role="grid"`/composite
34
+ * widgets. Left/Right/Home/End move within the header row; Up/Down/Home/End
35
+ * move within the body; Down from the header enters the body's roving stop,
36
+ * and Up from the body's first row returns to the header's roving stop.
37
+ * Enter/Space still only sort/activate (see `activateColumn()` /
38
+ * `activateRow()`).
39
+ *
40
+ * `columns[].priority` ('medium' | 'low') hides that column under
41
+ * `[part='base']`'s `@container` breakpoints; `[part='reveal-columns-button']`
42
+ * (rendered whenever any column has a priority) forces them all back into
43
+ * view. `columns[].sticky` pins a column's header/cells to the inline-start
44
+ * edge while the table scrolls horizontally.
22
45
  *
23
46
  * @customElement lyra-table
24
47
  * @event lyra-sort - A sortable header was activated. `detail: { key }`.
25
48
  * @event lyra-row-click - A row was activated. `detail: { row }`.
26
49
  * @event lyra-load-more - The "load more" control was activated.
27
- * @csspart base, table, head, header-cell, row, cell, more-button, sort-icon
50
+ * @csspart base, table, head, header-cell, row, cell, more-button, sort-icon, reveal-columns-button
28
51
  */
29
52
  export class LyraTable extends LyraElement {
30
53
  constructor() {
@@ -40,10 +63,32 @@ export class LyraTable extends LyraElement {
40
63
  this.emptyDescription = '';
41
64
  this.noColumnsHeading = 'No columns configured';
42
65
  this.noColumnsDescription = '';
66
+ this.revealColumnsLabel = 'Show all columns';
67
+ this.hideColumnsLabel = 'Show fewer columns';
68
+ /** Forces `priority`-hidden columns back into view, overriding the
69
+ * `@container` hide rules in table.styles.ts. Toggled by
70
+ * `[part='reveal-columns-button']`. */
71
+ this.showAllColumns = false;
72
+ /** Roving-tabindex position among header cells; `null` until a header is
73
+ * clicked/navigated to, at which point `focusedColKey()` falls back to
74
+ * the first column. */
75
+ this.activeColKey = null;
76
+ /** Roving-tabindex position among body rows; `null` until a row is
77
+ * clicked/navigated to, at which point `focusedRowKey()` falls back to
78
+ * `selectedKey` (if it matches a row) or the first row. */
79
+ this.activeRowKey = null;
43
80
  this.rowsByKey = new Map();
44
81
  this.columnsByKey = new Map();
82
+ this.toggleColumns = () => {
83
+ this.showAllColumns = !this.showAllColumns;
84
+ };
45
85
  this.onTableClick = (e) => {
46
86
  const target = e.target;
87
+ // A cell()-rendered button/link/input etc. owns its own click — don't let
88
+ // the delegated row/column resolution below re-interpret it as row or
89
+ // header activation.
90
+ if (target.closest(INTERACTIVE_SELECTOR))
91
+ return;
47
92
  const th = target.closest('[data-col-key]');
48
93
  if (th)
49
94
  return this.activateColumn(th.dataset.colKey);
@@ -52,25 +97,43 @@ export class LyraTable extends LyraElement {
52
97
  this.activateRow(tr.dataset.rowKey);
53
98
  };
54
99
  this.onTableKeyDown = (e) => {
55
- if (e.key !== 'Enter' && e.key !== ' ')
56
- return;
57
100
  const target = e.target;
101
+ // Same guard as onTableClick — also skips the table's own
102
+ // preventDefault(), so a focused nested control keeps its native/own
103
+ // Enter or Space activation instead of having it swallowed.
104
+ if (target.closest(INTERACTIVE_SELECTOR))
105
+ return;
58
106
  const th = target.closest('[data-col-key]');
59
- if (th) {
60
- e.preventDefault();
61
- return this.activateColumn(th.dataset.colKey);
62
- }
107
+ if (th)
108
+ return this.onHeaderKeyDown(e, th);
63
109
  const tr = target.closest('[data-row-key]');
64
- if (tr) {
65
- e.preventDefault();
66
- this.activateRow(tr.dataset.rowKey);
67
- }
110
+ if (tr)
111
+ this.onRowKeyDown(e, tr);
68
112
  };
69
113
  }
70
114
  static { this.styles = [LyraElement.styles, styles]; }
71
115
  keyOf(row, index) {
72
116
  return this.rowKey ? this.rowKey(row) : index;
73
117
  }
118
+ /** The header cell that currently owns `tabindex="0"`. */
119
+ focusedColKey() {
120
+ if (this.activeColKey !== null && this.columnsByKey.has(this.activeColKey)) {
121
+ return this.activeColKey;
122
+ }
123
+ return this.columns[0]?.key ?? null;
124
+ }
125
+ /** The body row that currently owns `tabindex="0"`. */
126
+ focusedRowKey() {
127
+ if (this.activeRowKey !== null && this.rowsByKey.has(this.activeRowKey)) {
128
+ return this.activeRowKey;
129
+ }
130
+ if (this.selectedKey !== null) {
131
+ const selected = String(this.selectedKey);
132
+ if (this.rowsByKey.has(selected))
133
+ return selected;
134
+ }
135
+ return this.rows.length > 0 ? String(this.keyOf(this.rows[0], 0)) : null;
136
+ }
74
137
  willUpdate(changed) {
75
138
  if (changed.has('rows') || changed.has('rowKey')) {
76
139
  this.rowsByKey = new Map(this.rows.map((row, i) => [String(this.keyOf(row, i)), row]));
@@ -80,15 +143,112 @@ export class LyraTable extends LyraElement {
80
143
  }
81
144
  }
82
145
  activateColumn(key) {
146
+ this.activeColKey = key;
83
147
  const col = this.columnsByKey.get(key);
84
148
  if (col?.sortable)
85
149
  this.emit('lyra-sort', { key: col.key });
86
150
  }
87
151
  activateRow(key) {
152
+ this.activeRowKey = key;
88
153
  const row = this.rowsByKey.get(key);
89
154
  if (row !== undefined)
90
155
  this.emit('lyra-row-click', { row });
91
156
  }
157
+ /** Header cells currently in the tab sequence — excludes columns hidden by
158
+ * a `priority`-driven `@container` rule (table.styles.ts), so Left/Right/
159
+ * Home/End never strand the roving tab stop on a `display: none` cell
160
+ * that `.focus()` would silently no-op on. */
161
+ visibleHeaders() {
162
+ return [...this.renderRoot.querySelectorAll('[data-col-key]')].filter((el) => el.offsetParent !== null);
163
+ }
164
+ focusHeader(el) {
165
+ if (!el?.dataset.colKey)
166
+ return;
167
+ this.activeColKey = el.dataset.colKey;
168
+ el.focus();
169
+ }
170
+ focusRow(el) {
171
+ if (!el?.dataset.rowKey)
172
+ return;
173
+ this.activeRowKey = el.dataset.rowKey;
174
+ el.focus();
175
+ }
176
+ onHeaderKeyDown(e, th) {
177
+ if (e.key === 'Enter' || e.key === ' ') {
178
+ e.preventDefault();
179
+ this.activateColumn(th.dataset.colKey);
180
+ return;
181
+ }
182
+ const headers = this.visibleHeaders();
183
+ const index = headers.indexOf(th);
184
+ if (index < 0)
185
+ return;
186
+ switch (e.key) {
187
+ case 'ArrowLeft':
188
+ e.preventDefault();
189
+ this.focusHeader(headers[Math.max(0, index - 1)]);
190
+ return;
191
+ case 'ArrowRight':
192
+ e.preventDefault();
193
+ this.focusHeader(headers[Math.min(headers.length - 1, index + 1)]);
194
+ return;
195
+ case 'Home':
196
+ e.preventDefault();
197
+ this.focusHeader(headers[0]);
198
+ return;
199
+ case 'End':
200
+ e.preventDefault();
201
+ this.focusHeader(headers[headers.length - 1]);
202
+ return;
203
+ case 'ArrowDown': {
204
+ e.preventDefault();
205
+ const rows = [...this.renderRoot.querySelectorAll('[data-row-key]')];
206
+ const key = this.focusedRowKey();
207
+ this.focusRow(rows.find((r) => r.dataset.rowKey === key) ?? rows[0] ?? null);
208
+ return;
209
+ }
210
+ default:
211
+ return;
212
+ }
213
+ }
214
+ onRowKeyDown(e, tr) {
215
+ if (e.key === 'Enter' || e.key === ' ') {
216
+ e.preventDefault();
217
+ this.activateRow(tr.dataset.rowKey);
218
+ return;
219
+ }
220
+ const bodyRows = [...this.renderRoot.querySelectorAll('[data-row-key]')];
221
+ const index = bodyRows.indexOf(tr);
222
+ if (index < 0)
223
+ return;
224
+ switch (e.key) {
225
+ case 'ArrowDown':
226
+ e.preventDefault();
227
+ this.focusRow(bodyRows[Math.min(bodyRows.length - 1, index + 1)]);
228
+ return;
229
+ case 'ArrowUp':
230
+ e.preventDefault();
231
+ if (index === 0) {
232
+ const headers = this.visibleHeaders();
233
+ const key = this.focusedColKey();
234
+ this.focusHeader(headers.find((h) => h.dataset.colKey === key) ?? headers[0] ?? null);
235
+ }
236
+ else {
237
+ this.focusRow(bodyRows[index - 1]);
238
+ }
239
+ return;
240
+ case 'Home':
241
+ e.preventDefault();
242
+ this.focusRow(bodyRows[0]);
243
+ return;
244
+ case 'End':
245
+ e.preventDefault();
246
+ this.focusRow(bodyRows[bodyRows.length - 1]);
247
+ return;
248
+ default:
249
+ return;
250
+ }
251
+ }
92
252
  render() {
93
253
  if (this.columns.length === 0) {
94
254
  return html `<lyra-empty
@@ -102,8 +262,11 @@ export class LyraTable extends LyraElement {
102
262
  description=${this.emptyDescription}
103
263
  ></lyra-empty>`;
104
264
  }
265
+ const hasPriorityColumns = this.columns.some((col) => col.priority);
266
+ const focusedCol = this.focusedColKey();
267
+ const focusedRow = this.focusedRowKey();
105
268
  return html `
106
- <div part="base">
269
+ <div part="base" ?data-force-visible=${this.showAllColumns}>
107
270
  <table part="table" role="grid" @click=${this.onTableClick} @keydown=${this.onTableKeyDown}>
108
271
  <thead part="head">
109
272
  <tr role="row">
@@ -116,9 +279,11 @@ export class LyraTable extends LyraElement {
116
279
  scope="col"
117
280
  data-col-key=${col.key}
118
281
  data-align=${col.align ?? 'start'}
282
+ data-priority=${col.priority ?? nothing}
283
+ ?data-sticky=${col.sticky}
119
284
  ?data-sortable=${col.sortable}
120
285
  aria-sort=${col.sortable ? ariaSort : nothing}
121
- tabindex=${col.sortable ? '0' : '-1'}
286
+ tabindex=${col.key === focusedCol ? '0' : '-1'}
122
287
  >
123
288
  ${col.label}
124
289
  ${active
@@ -139,15 +304,31 @@ export class LyraTable extends LyraElement {
139
304
  role="row"
140
305
  data-row-key=${String(key)}
141
306
  aria-selected=${selected ? 'true' : 'false'}
142
- tabindex="0"
307
+ tabindex=${String(key) === focusedRow ? '0' : '-1'}
143
308
  >
144
- ${this.columns.map((col) => html `<td part="cell" role="gridcell" data-align=${col.align ?? 'start'}>
309
+ ${this.columns.map((col) => html `<td
310
+ part="cell"
311
+ role="gridcell"
312
+ data-align=${col.align ?? 'start'}
313
+ data-priority=${col.priority ?? nothing}
314
+ ?data-sticky=${col.sticky}
315
+ >
145
316
  ${col.cell(row)}
146
317
  </td>`)}
147
318
  </tr>`;
148
319
  })}
149
320
  </tbody>
150
321
  </table>
322
+ ${hasPriorityColumns
323
+ ? html `<button
324
+ part="reveal-columns-button"
325
+ type="button"
326
+ aria-pressed=${this.showAllColumns ? 'true' : 'false'}
327
+ @click=${this.toggleColumns}
328
+ >
329
+ ${this.showAllColumns ? this.hideColumnsLabel : this.revealColumnsLabel}
330
+ </button>`
331
+ : nothing}
151
332
  ${this.hasMore
152
333
  ? html `<button
153
334
  part="more-button"
@@ -197,4 +378,19 @@ __decorate([
197
378
  __decorate([
198
379
  property({ attribute: 'no-columns-description' })
199
380
  ], LyraTable.prototype, "noColumnsDescription", void 0);
381
+ __decorate([
382
+ property({ attribute: 'reveal-columns-label' })
383
+ ], LyraTable.prototype, "revealColumnsLabel", void 0);
384
+ __decorate([
385
+ property({ attribute: 'hide-columns-label' })
386
+ ], LyraTable.prototype, "hideColumnsLabel", void 0);
387
+ __decorate([
388
+ state()
389
+ ], LyraTable.prototype, "showAllColumns", void 0);
390
+ __decorate([
391
+ state()
392
+ ], LyraTable.prototype, "activeColKey", void 0);
393
+ __decorate([
394
+ state()
395
+ ], LyraTable.prototype, "activeRowKey", void 0);
200
396
  defineElement('table', LyraTable);
@@ -4,3 +4,9 @@ export default meta;
4
4
  type Story = StoryObj;
5
5
  export declare const Default: Story;
6
6
  export declare const Empty: Story;
7
+ export declare const NoColumnsConfigured: Story;
8
+ export declare const ActiveSort: Story;
9
+ export declare const SelectedRow: Story;
10
+ export declare const LoadMore: Story;
11
+ export declare const PriorityAndSticky: Story;
12
+ export declare const RowActions: Story;
@@ -20,3 +20,48 @@ export const Default = {
20
20
  export const Empty = {
21
21
  render: () => html `<lyra-table .columns=${columns} .rows=${[]}></lyra-table>`,
22
22
  };
23
+ export const NoColumnsConfigured = {
24
+ render: () => html `<lyra-table .columns=${[]} .rows=${rows}></lyra-table>`,
25
+ };
26
+ export const ActiveSort = {
27
+ render: () => html `<lyra-table .columns=${columns} .rows=${rows} sort-key="score" sort-dir="desc"></lyra-table>`,
28
+ };
29
+ export const SelectedRow = {
30
+ render: () => html `<lyra-table .columns=${columns} .rows=${rows} .selectedKey=${'b'}></lyra-table>`,
31
+ };
32
+ export const LoadMore = {
33
+ render: () => html `<lyra-table .columns=${columns} .rows=${rows} has-more more-label="Load more rows"></lyra-table>`,
34
+ };
35
+ const detailRows = [
36
+ { id: 'a', name: 'Alpha', score: 92, region: 'EU-West', updated: '2 min ago' },
37
+ { id: 'b', name: 'Beta', score: 81, region: 'US-East', updated: '5 min ago' },
38
+ { id: 'c', name: 'Gamma', score: 76, region: 'AP-South', updated: '1 hr ago' },
39
+ ];
40
+ // Narrow the story's own container, so the `priority`-hidden columns below
41
+ // actually hide without needing to shrink the whole Storybook viewport.
42
+ const priorityColumns = [
43
+ { key: 'name', label: 'Name', sortable: true, sticky: true, cell: (r) => r.name },
44
+ { key: 'score', label: 'Score', sortable: true, align: 'end', cell: (r) => r.score },
45
+ { key: 'region', label: 'Region', priority: 'medium', cell: (r) => r.region },
46
+ { key: 'updated', label: 'Updated', priority: 'low', cell: (r) => r.updated },
47
+ ];
48
+ export const PriorityAndSticky = {
49
+ render: () => html `<div style="max-width: 420px;">
50
+ <lyra-table .columns=${priorityColumns} .rows=${detailRows}></lyra-table>
51
+ </div>`,
52
+ };
53
+ // A <button> inside cell() must own its own click/Enter activation — the
54
+ // table's row-click delegation must not intercept it (see table.ts's
55
+ // INTERACTIVE_SELECTOR guard).
56
+ const actionColumns = [
57
+ { key: 'name', label: 'Name', cell: (r) => r.name },
58
+ { key: 'score', label: 'Score', align: 'end', cell: (r) => r.score },
59
+ {
60
+ key: 'actions',
61
+ label: 'Actions',
62
+ cell: (r) => html `<button type="button" @click=${() => alert(`Editing ${r.name}`)}>Edit</button>`,
63
+ },
64
+ ];
65
+ export const RowActions = {
66
+ render: () => html `<lyra-table .columns=${actionColumns} .rows=${rows}></lyra-table>`,
67
+ };
@@ -9,6 +9,29 @@ export const styles = css `
9
9
  max-block-size: var(--lyra-table-max-height, none);
10
10
  border: 1px solid var(--lyra-color-border);
11
11
  border-radius: var(--lyra-radius);
12
+ /* Makes [part='base'] a query container so the @container rules below can
13
+ react to the table's own available width instead of the viewport's. */
14
+ container-type: inline-size;
15
+ }
16
+ /*
17
+ * Column priority (columns[].priority) hides [data-priority='low']/
18
+ * ['medium'] header/cells as the container narrows. showAllColumns
19
+ * (toggled by [part='reveal-columns-button']) needs to override this, but
20
+ * a @container query can only condition on ancestor inline-size, not
21
+ * component state — so it's surfaced instead as the data-force-visible
22
+ * attribute on [part='base'] itself, and the hide rule's :not(...)
23
+ * selector excludes it. Toggling the attribute flips every hidden column
24
+ * back on without a second, state-aware container query.
25
+ */
26
+ @container (max-width: 899.98px) {
27
+ [part='base']:not([data-force-visible]) [data-priority='low'] {
28
+ display: none;
29
+ }
30
+ }
31
+ @container (max-width: 639.98px) {
32
+ [part='base']:not([data-force-visible]) [data-priority='medium'] {
33
+ display: none;
34
+ }
12
35
  }
13
36
  [part='table'] {
14
37
  inline-size: 100%;
@@ -33,7 +56,10 @@ export const styles = css `
33
56
  [part='header-cell'][data-sortable]:hover {
34
57
  background: var(--lyra-color-brand-quiet);
35
58
  }
36
- [part='header-cell'][data-sortable]:focus-visible {
59
+ /* Not scoped to [data-sortable] — the roving-tabindex header stop (see
60
+ table.ts's focusedColKey()) can land on any column, sortable or not, so
61
+ every header cell needs its own visible focus indicator. */
62
+ [part='header-cell']:focus-visible {
37
63
  outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
38
64
  outline-offset: var(--lyra-focus-ring-offset);
39
65
  }
@@ -59,6 +85,11 @@ export const styles = css `
59
85
  [part='sort-icon'][data-dir='desc'] {
60
86
  transform: rotate(90deg);
61
87
  }
88
+ @media (prefers-reduced-motion: reduce) {
89
+ [part='sort-icon'] {
90
+ transition: none !important;
91
+ }
92
+ }
62
93
  [part='row']:hover {
63
94
  background: var(--lyra-color-brand-quiet);
64
95
  }
@@ -76,7 +107,21 @@ export const styles = css `
76
107
  [part='cell'][data-align='end'] {
77
108
  text-align: end;
78
109
  }
79
- [part='more-button'] {
110
+ /* columns[].sticky pins a column's header/cells to the inline-start edge
111
+ while the table scrolls horizontally — mirrors [part='header-cell']'s
112
+ existing inset-block-start vertical-scroll sticky pattern above, just on
113
+ the other axis. The box-shadow is the seam that separates it from
114
+ content scrolled underneath it. */
115
+ [part='header-cell'][data-sticky],
116
+ [part='cell'][data-sticky] {
117
+ position: sticky;
118
+ inset-inline-start: 0;
119
+ z-index: 1;
120
+ background: var(--lyra-color-surface);
121
+ box-shadow: 1px 0 0 0 var(--lyra-color-border);
122
+ }
123
+ [part='more-button'],
124
+ [part='reveal-columns-button'] {
80
125
  display: block;
81
126
  inline-size: 100%;
82
127
  padding: var(--lyra-space-s);
@@ -87,10 +132,12 @@ export const styles = css `
87
132
  cursor: pointer;
88
133
  border-block-start: 1px solid var(--lyra-color-border);
89
134
  }
90
- [part='more-button']:hover {
135
+ [part='more-button']:hover,
136
+ [part='reveal-columns-button']:hover {
91
137
  background: var(--lyra-color-brand-quiet);
92
138
  }
93
- [part='more-button']:focus-visible {
139
+ [part='more-button']:focus-visible,
140
+ [part='reveal-columns-button']:focus-visible {
94
141
  outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
95
142
  outline-offset: var(--lyra-focus-ring-offset);
96
143
  }