@aceshooting/lyra-ui 1.0.0 → 1.1.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 (148) hide show
  1. package/README.md +8 -9
  2. package/custom-elements.json +10463 -4519
  3. package/dist/components/chart/chart.d.ts +46 -1
  4. package/dist/components/chart/chart.js +148 -10
  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 +74 -0
  13. package/dist/components/chart/lite-chart.js +384 -0
  14. package/dist/components/chart/lite-chart.stories.d.ts +12 -0
  15. package/dist/components/chart/lite-chart.stories.js +98 -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 +75 -12
  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 +48 -3
  50. package/dist/components/flag/flag.js +65 -29
  51. package/dist/components/flag/flag.styles.js +1 -1
  52. package/dist/components/flag/language-map.d.ts +11 -0
  53. package/dist/components/flag/language-map.js +12 -1
  54. package/dist/components/gauge/gauge.d.ts +2 -0
  55. package/dist/components/gauge/gauge.js +19 -5
  56. package/dist/components/gauge/gauge.stories.d.ts +2 -0
  57. package/dist/components/gauge/gauge.stories.js +6 -0
  58. package/dist/components/graph/graph-loader.d.ts +28 -0
  59. package/dist/components/graph/graph-loader.js +44 -0
  60. package/dist/components/graph/graph.d.ts +41 -15
  61. package/dist/components/graph/graph.js +146 -71
  62. package/dist/components/graph/graph.stories.d.ts +3 -0
  63. package/dist/components/graph/graph.stories.js +38 -0
  64. package/dist/components/graph/graph.styles.js +6 -1
  65. package/dist/components/heatmap/calendar-grid.d.ts +7 -0
  66. package/dist/components/heatmap/calendar-grid.js +27 -10
  67. package/dist/components/heatmap/heatmap.d.ts +137 -12
  68. package/dist/components/heatmap/heatmap.js +427 -27
  69. package/dist/components/heatmap/heatmap.stories.d.ts +17 -0
  70. package/dist/components/heatmap/heatmap.stories.js +130 -0
  71. package/dist/components/heatmap/heatmap.styles.js +60 -2
  72. package/dist/components/map/map.d.ts +17 -1
  73. package/dist/components/map/map.js +68 -21
  74. package/dist/components/map/map.stories.d.ts +10 -0
  75. package/dist/components/map/map.stories.js +107 -12
  76. package/dist/components/map/map.styles.js +2 -2
  77. package/dist/components/playback/playback.d.ts +6 -2
  78. package/dist/components/playback/playback.js +36 -12
  79. package/dist/components/playback/playback.stories.d.ts +2 -0
  80. package/dist/components/playback/playback.stories.js +6 -0
  81. package/dist/components/playback/playback.styles.js +10 -2
  82. package/dist/components/select/select.d.ts +122 -0
  83. package/dist/components/select/select.js +516 -0
  84. package/dist/components/select/select.stories.d.ts +19 -0
  85. package/dist/components/select/select.stories.js +75 -0
  86. package/dist/components/select/select.styles.d.ts +1 -0
  87. package/dist/components/select/select.styles.js +188 -0
  88. package/dist/components/skeleton/skeleton.js +4 -7
  89. package/dist/components/sparkline/sparkline.d.ts +1 -1
  90. package/dist/components/sparkline/sparkline.js +40 -13
  91. package/dist/components/sparkline/sparkline.stories.d.ts +4 -0
  92. package/dist/components/sparkline/sparkline.stories.js +22 -0
  93. package/dist/components/split/split.d.ts +24 -3
  94. package/dist/components/split/split.js +155 -31
  95. package/dist/components/split/split.stories.d.ts +2 -0
  96. package/dist/components/split/split.stories.js +20 -0
  97. package/dist/components/stat/stat.d.ts +27 -1
  98. package/dist/components/stat/stat.js +56 -3
  99. package/dist/components/stat/stat.stories.d.ts +3 -0
  100. package/dist/components/stat/stat.stories.js +49 -1
  101. package/dist/components/stat/stat.styles.js +44 -0
  102. package/dist/components/table/table.d.ts +64 -2
  103. package/dist/components/table/table.js +213 -17
  104. package/dist/components/table/table.stories.d.ts +6 -0
  105. package/dist/components/table/table.stories.js +45 -0
  106. package/dist/components/table/table.styles.js +51 -4
  107. package/dist/components/time-range/time-range.d.ts +34 -5
  108. package/dist/components/time-range/time-range.js +217 -36
  109. package/dist/components/time-range/time-range.stories.d.ts +2 -0
  110. package/dist/components/time-range/time-range.stories.js +13 -0
  111. package/dist/components/time-range/time-range.styles.js +44 -2
  112. package/dist/components/toast/toast-item.d.ts +14 -2
  113. package/dist/components/toast/toast-item.js +134 -17
  114. package/dist/components/toast/toast-item.styles.js +28 -2
  115. package/dist/components/toast/toast.stories.d.ts +3 -0
  116. package/dist/components/toast/toast.stories.js +41 -1
  117. package/dist/components/toast/toast.styles.js +11 -11
  118. package/dist/components/toast/toaster.d.ts +1 -1
  119. package/dist/components/toast/toaster.js +13 -6
  120. package/dist/components/tree/tree-node.d.ts +13 -7
  121. package/dist/components/tree/tree-node.js +24 -10
  122. package/dist/components/tree/tree.d.ts +64 -15
  123. package/dist/components/tree/tree.js +113 -22
  124. package/dist/components/tree/tree.stories.d.ts +4 -0
  125. package/dist/components/tree/tree.stories.js +31 -1
  126. package/dist/components/tree/update-cascade.d.ts +13 -0
  127. package/dist/components/tree/update-cascade.js +13 -0
  128. package/dist/components/widget/widget.d.ts +15 -1
  129. package/dist/components/widget/widget.js +140 -5
  130. package/dist/components/widget/widget.stories.d.ts +1 -0
  131. package/dist/components/widget/widget.stories.js +10 -0
  132. package/dist/components/widget/widget.styles.js +11 -1
  133. package/dist/components/word-cloud/word-cloud-layout.d.ts +63 -0
  134. package/dist/components/word-cloud/word-cloud-layout.js +145 -0
  135. package/dist/components/word-cloud/word-cloud.d.ts +65 -0
  136. package/dist/components/word-cloud/word-cloud.js +255 -0
  137. package/dist/components/word-cloud/word-cloud.stories.d.ts +10 -0
  138. package/dist/components/word-cloud/word-cloud.stories.js +55 -0
  139. package/dist/components/word-cloud/word-cloud.styles.d.ts +1 -0
  140. package/dist/components/word-cloud/word-cloud.styles.js +54 -0
  141. package/dist/internal/a11y.js +2 -1
  142. package/dist/internal/rtl.d.ts +7 -0
  143. package/dist/internal/rtl.js +9 -0
  144. package/dist/lyra.d.ts +9 -1
  145. package/dist/lyra.js +6 -0
  146. package/llms-full.txt +892 -276
  147. package/llms.txt +12 -9
  148. package/package.json +6 -4
@@ -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
  }
@@ -1,14 +1,25 @@
1
1
  import { type TemplateResult, type PropertyValues } from 'lit';
2
2
  import { LyraElement } from '../../internal/lyra-element.js';
3
+ /** A single discrete-preset option for the `presets` property. */
4
+ export interface TimeRangePreset {
5
+ label: string;
6
+ start: number;
7
+ end: number;
8
+ }
3
9
  /**
4
10
  * `<lyra-time-range>` — a two-handle brush/scrubber over a numeric domain.
5
11
  * Callers map their own time axis to `[min, max]`; no date logic lives here
6
12
  * (matches the no-date-library constraint used elsewhere in this library).
7
13
  *
14
+ * Optionally paired with a row of discrete presets (`presets`) — e.g. "Last
15
+ * 7 days" / "Last 30 days" — rendered above the track; picking one is just a
16
+ * shortcut that sets both handles at once, the continuous brush underneath
17
+ * is unaffected and both interaction modes coexist.
18
+ *
8
19
  * @customElement lyra-time-range
9
- * @event lyra-input - Fired continuously while dragging. `detail: { start, end }`.
10
- * @event lyra-change - Fired on release / keyup-commit. `detail: { start, end }`.
11
- * @csspart base, track, range, handle-start, handle-end
20
+ * @event lyra-input - Fired continuously while dragging or on each arrow-key press. `detail: { start, end }`.
21
+ * @event lyra-change - Fired on release / keyup-commit, or when a preset button is clicked. `detail: { start, end }`.
22
+ * @csspart base, track, range, handle-start, handle-end, presets, preset-button
12
23
  */
13
24
  export declare class LyraTimeRange extends LyraElement {
14
25
  static styles: import("lit").CSSResultGroup[];
@@ -18,17 +29,35 @@ export declare class LyraTimeRange extends LyraElement {
18
29
  end: number;
19
30
  step: number;
20
31
  disabled: boolean;
21
- private dragging;
32
+ /**
33
+ * Optional discrete presets rendered as a `[part="presets"]` button row
34
+ * above the track. Purely additive: leaving this empty (the default)
35
+ * renders nothing extra and leaves the continuous brush untouched.
36
+ */
37
+ presets: TimeRangePreset[];
38
+ private drags;
22
39
  disconnectedCallback(): void;
40
+ private domain;
23
41
  private percentOf;
42
+ /** Each handle's actual reachable sub-range, bounded by its sibling
43
+ * handle's current value the same way clamp() already enforces it — used
44
+ * both for aria-valuemin/aria-valuemax and for Home/End's jump targets,
45
+ * instead of reporting/jumping to the component's full [min, max] domain
46
+ * which the sibling handle may make partially unreachable. */
47
+ private reachableBounds;
24
48
  private clamp;
25
49
  private setValue;
50
+ /**
51
+ * Apply a discrete preset: sets both handles and emits the same
52
+ * lyra-input/lyra-change pair a committed drag or keyboard step would.
53
+ */
54
+ private applyPreset;
26
55
  private onKeyDown;
27
56
  private onKeyUp;
28
57
  private onPointerDown;
29
58
  private onPointerMove;
30
59
  private onPointerUp;
31
- /** Stop an in-progress drag, optionally committing a final lyra-change. */
60
+ /** Stop the drag owned by `pointerId`, optionally committing a final lyra-change. */
32
61
  private endDrag;
33
62
  protected willUpdate(changed: PropertyValues): void;
34
63
  render(): TemplateResult;