@haiyue/ui 0.1.2 → 0.2.1

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 (95) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/README.md +444 -1
  3. package/dist/adapters/web/controls.d.ts +19 -0
  4. package/dist/adapters/web/controls.js +284 -0
  5. package/dist/adapters/web/dom.d.ts +10 -0
  6. package/dist/adapters/web/dom.js +98 -0
  7. package/dist/adapters/web/index.d.ts +3 -0
  8. package/dist/adapters/web/index.js +3 -0
  9. package/dist/adapters/web/renderer.d.ts +18 -0
  10. package/dist/adapters/web/renderer.js +545 -0
  11. package/dist/adapters/web/styles.d.ts +3 -0
  12. package/dist/adapters/web/styles.js +62 -0
  13. package/dist/adapters/web/types.d.ts +46 -0
  14. package/dist/adapters/web/types.js +2 -0
  15. package/dist/affix.d.ts +35 -0
  16. package/dist/affix.js +137 -0
  17. package/dist/badge.d.ts +45 -0
  18. package/dist/badge.js +168 -0
  19. package/dist/breadcrumb.d.ts +37 -0
  20. package/dist/breadcrumb.js +210 -0
  21. package/dist/button-group.d.ts +12 -0
  22. package/dist/button-group.js +72 -0
  23. package/dist/button.d.ts +15 -1
  24. package/dist/button.js +101 -12
  25. package/dist/cascader.d.ts +65 -0
  26. package/dist/cascader.js +479 -0
  27. package/dist/collapse.d.ts +55 -0
  28. package/dist/collapse.js +261 -0
  29. package/dist/core/actions.d.ts +32 -0
  30. package/dist/core/actions.js +145 -0
  31. package/dist/core/data.d.ts +4 -0
  32. package/dist/core/data.js +27 -0
  33. package/dist/core/index.d.ts +7 -0
  34. package/dist/core/index.js +6 -0
  35. package/dist/core/platform-view.d.ts +17 -0
  36. package/dist/core/platform-view.js +84 -0
  37. package/dist/core/selection.d.ts +14 -0
  38. package/dist/core/selection.js +93 -0
  39. package/dist/core/text.d.ts +31 -0
  40. package/dist/core/text.js +154 -0
  41. package/dist/core/types.d.ts +250 -0
  42. package/dist/core/types.js +2 -0
  43. package/dist/core/validation.d.ts +24 -0
  44. package/dist/core/validation.js +238 -0
  45. package/dist/date-picker.d.ts +60 -0
  46. package/dist/date-picker.js +355 -0
  47. package/dist/dialog.d.ts +4 -0
  48. package/dist/dialog.js +23 -5
  49. package/dist/divider.d.ts +33 -0
  50. package/dist/divider.js +113 -0
  51. package/dist/draggable.d.ts +78 -0
  52. package/dist/draggable.js +255 -0
  53. package/dist/index.d.ts +45 -1
  54. package/dist/index.js +66 -0
  55. package/dist/input.d.ts +30 -1
  56. package/dist/input.js +166 -54
  57. package/dist/portable/index.d.ts +4 -0
  58. package/dist/portable/index.js +3 -0
  59. package/dist/portable/page.d.ts +38 -0
  60. package/dist/portable/page.js +361 -0
  61. package/dist/portable/theme.d.ts +19 -0
  62. package/dist/portable/theme.js +40 -0
  63. package/dist/portable/tree.d.ts +10 -0
  64. package/dist/portable/tree.js +96 -0
  65. package/dist/range.d.ts +3 -0
  66. package/dist/range.js +16 -5
  67. package/dist/rate.d.ts +46 -0
  68. package/dist/rate.js +193 -0
  69. package/dist/resizable.d.ts +97 -0
  70. package/dist/resizable.js +426 -0
  71. package/dist/skeleton.d.ts +41 -0
  72. package/dist/skeleton.js +141 -0
  73. package/dist/steps.d.ts +45 -0
  74. package/dist/steps.js +203 -0
  75. package/dist/table.d.ts +106 -0
  76. package/dist/table.js +689 -0
  77. package/dist/tabs.d.ts +3 -0
  78. package/dist/tabs.js +11 -1
  79. package/dist/tag.d.ts +55 -0
  80. package/dist/tag.js +225 -0
  81. package/dist/time-picker.d.ts +62 -0
  82. package/dist/time-picker.js +298 -0
  83. package/dist/timeline.d.ts +48 -0
  84. package/dist/timeline.js +201 -0
  85. package/dist/toast.d.ts +55 -0
  86. package/dist/toast.js +224 -0
  87. package/dist/tour.d.ts +89 -0
  88. package/dist/tour.js +306 -0
  89. package/dist/virtual-keyboard.d.ts +95 -0
  90. package/dist/virtual-keyboard.js +432 -0
  91. package/dist/virtual-mouse.d.ts +82 -0
  92. package/dist/virtual-mouse.js +358 -0
  93. package/dist/watermark.d.ts +44 -0
  94. package/dist/watermark.js +148 -0
  95. package/package.json +115 -4
package/dist/table.js ADDED
@@ -0,0 +1,689 @@
1
+ const DEFAULT_PAGE_SIZE = 10;
2
+ const DEFAULT_PAGE_SIZE_OPTIONS = [5, 10, 20, 50];
3
+ const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
4
+ function finiteInteger(value, fallback, minimum = 1) {
5
+ const number = Number(value);
6
+ return Number.isFinite(number) ? Math.max(minimum, Math.floor(number)) : fallback;
7
+ }
8
+ function positiveIntegerOptions(value) {
9
+ return [...new Set(value
10
+ .map(item => Number(item))
11
+ .filter(item => Number.isFinite(item) && item > 0)
12
+ .map(item => Math.floor(item)))]
13
+ .sort((left, right) => left - right);
14
+ }
15
+ function readValue(record, path) {
16
+ let value = record;
17
+ for (const segment of path.split('.')) {
18
+ if (!value || typeof value !== 'object')
19
+ return undefined;
20
+ value = value[segment];
21
+ }
22
+ return value;
23
+ }
24
+ function defaultCompare(left, right) {
25
+ if (typeof left === 'number' && typeof right === 'number')
26
+ return left - right;
27
+ if (left == null && right == null)
28
+ return 0;
29
+ if (left == null)
30
+ return -1;
31
+ if (right == null)
32
+ return 1;
33
+ return collator.compare(String(left), String(right));
34
+ }
35
+ function partToken(value) {
36
+ return value.toLocaleLowerCase().replace(/[^a-z0-9_-]+/gu, '-').replace(/^-+|-+$/gu, '') || 'column';
37
+ }
38
+ function isSortable(column) {
39
+ return Boolean(column.sortable || column.sorter);
40
+ }
41
+ function isFilterable(column) {
42
+ return Boolean(column.filterable || column.filterOptions || column.filter);
43
+ }
44
+ /** Local-data table with column filters, stable sorting, and client-side pagination. */
45
+ export class HYTable extends HTMLElement {
46
+ _scroller = document.createElement('div');
47
+ _table = document.createElement('table');
48
+ _head = document.createElement('thead');
49
+ _body = document.createElement('tbody');
50
+ _pagination = document.createElement('nav');
51
+ _columns = [];
52
+ _dataSource = [];
53
+ _filters = {};
54
+ _sort = null;
55
+ _page = 1;
56
+ _pageSize = DEFAULT_PAGE_SIZE;
57
+ _pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS;
58
+ _reflecting = false;
59
+ static get observedAttributes() {
60
+ return ['page', 'page-size', 'page-size-options', 'pagination', 'show-filters', 'aria-label'];
61
+ }
62
+ constructor() {
63
+ super();
64
+ const root = this.attachShadow({ mode: 'open' });
65
+ const style = document.createElement('style');
66
+ style.textContent = `
67
+ :host {
68
+ display: block;
69
+ min-width: 0;
70
+ color: var(--hy-text-color, #eef3ff);
71
+ font: 13px/1.4 system-ui, sans-serif;
72
+ }
73
+ .scroller {
74
+ overflow: auto;
75
+ border: 1px solid var(--hy-border-color, #3d4654);
76
+ border-radius: var(--hy-table-radius, 7px);
77
+ background: var(--hy-surface-color, #171d27);
78
+ scrollbar-color: var(--hy-border-strong-color, #596779) var(--hy-input-bg-color, #111722);
79
+ scrollbar-width: thin;
80
+ }
81
+ table { width: 100%; border-collapse: collapse; table-layout: var(--hy-table-layout, auto); }
82
+ th, td {
83
+ box-sizing: border-box;
84
+ min-width: 0;
85
+ padding: var(--hy-table-cell-padding, 11px 13px);
86
+ border-bottom: 1px solid var(--hy-border-subtle-color, var(--hy-border-color, #3d4654));
87
+ overflow-wrap: anywhere;
88
+ }
89
+ th {
90
+ position: relative;
91
+ color: var(--hy-secondary-text-color, #aebbd0);
92
+ background: var(--hy-table-header-background, var(--hy-input-bg-color, #111722));
93
+ font-size: 11px;
94
+ font-weight: 650;
95
+ text-align: left;
96
+ }
97
+ tbody tr { background: var(--hy-table-row-background, var(--hy-surface-color, #171d27)); }
98
+ tbody tr:hover { background: var(--hy-table-row-hover-background, var(--hy-hover-bg-color, #273241)); }
99
+ tbody tr:last-child td { border-bottom: 0; }
100
+ thead { position: relative; z-index: 2; }
101
+ tbody { position: relative; z-index: 1; }
102
+ .header-content { display: flex; align-items: center; gap: 7px; min-width: 0; }
103
+ .header-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
104
+ .sort-button {
105
+ display: inline-flex;
106
+ align-items: center;
107
+ gap: 7px;
108
+ max-width: 100%;
109
+ padding: 0;
110
+ border: 0;
111
+ color: inherit;
112
+ background: transparent;
113
+ cursor: pointer;
114
+ font: inherit;
115
+ text-align: inherit;
116
+ }
117
+ .sort-button:hover, .sort-button:focus-visible { color: var(--hy-accent-color, #79a8ff); }
118
+ .sort-indicator { color: var(--hy-muted-text-color, #667587); font-size: 9px; }
119
+ th[aria-sort="ascending"] .sort-indicator,
120
+ th[aria-sort="descending"] .sort-indicator { color: var(--hy-accent-color, #79a8ff); }
121
+ .filter-menu { position: relative; flex: 0 0 auto; }
122
+ .filter-trigger {
123
+ display: grid;
124
+ width: 24px;
125
+ height: 24px;
126
+ box-sizing: border-box;
127
+ place-items: center;
128
+ padding: 0;
129
+ border: 1px solid transparent;
130
+ border-radius: var(--hy-table-filter-radius, 5px);
131
+ color: var(--hy-muted-text-color, #667587);
132
+ background: transparent;
133
+ cursor: pointer;
134
+ list-style: none;
135
+ }
136
+ .filter-trigger::-webkit-details-marker { display: none; }
137
+ .filter-trigger svg { width: 13px; height: 13px; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 1.8; }
138
+ .filter-trigger:hover, .filter-trigger:focus-visible, .filter-menu[open] .filter-trigger {
139
+ border-color: var(--hy-border-color, #3d4654);
140
+ color: var(--hy-accent-color, #79a8ff);
141
+ background: var(--hy-hover-bg-color, #273241);
142
+ outline: none;
143
+ }
144
+ .filter-trigger[data-active="true"] { color: var(--hy-accent-color, #79a8ff); }
145
+ .filter-panel {
146
+ position: absolute;
147
+ top: calc(100% + 7px);
148
+ left: 0;
149
+ z-index: 5;
150
+ display: flex;
151
+ width: max-content;
152
+ min-width: 184px;
153
+ max-width: min(280px, 70vw);
154
+ gap: 7px;
155
+ box-sizing: border-box;
156
+ padding: 9px;
157
+ border: 1px solid var(--hy-border-color, #3d4654);
158
+ border-radius: var(--hy-table-filter-panel-radius, 7px);
159
+ background: var(--hy-surface-elevated-color, var(--hy-surface-color, #171d27));
160
+ box-shadow: 0 10px 28px var(--hy-menu-shadow-color, rgba(0, 0, 0, 0.28));
161
+ }
162
+ .filter-menu:not([open]) .filter-panel { display: none; }
163
+ th:last-child .filter-panel { right: 0; left: auto; }
164
+ .filter {
165
+ flex: 1 1 auto;
166
+ width: 150px;
167
+ min-width: 74px;
168
+ height: 30px;
169
+ box-sizing: border-box;
170
+ padding: 0 9px;
171
+ border: 1px solid var(--hy-border-color, #3d4654);
172
+ border-radius: var(--hy-table-filter-radius, 5px);
173
+ outline: none;
174
+ color: var(--hy-text-color, #eef3ff);
175
+ background: var(--hy-input-bg-color, #111722);
176
+ font: 11px system-ui, sans-serif;
177
+ }
178
+ .filter:focus { border-color: var(--hy-focus-border-color, #79a8ff); box-shadow: 0 0 0 2px var(--hy-focus-ring-color, rgba(121, 168, 255, 0.22)); }
179
+ .filter-clear {
180
+ flex: 0 0 auto;
181
+ height: 30px;
182
+ padding: 0 9px;
183
+ border: 1px solid var(--hy-border-color, #3d4654);
184
+ border-radius: var(--hy-table-filter-radius, 5px);
185
+ color: var(--hy-secondary-text-color, #aebbd0);
186
+ background: var(--hy-surface-color, #171d27);
187
+ cursor: pointer;
188
+ font: 11px system-ui, sans-serif;
189
+ }
190
+ .filter-clear:hover:not(:disabled) { border-color: var(--hy-accent-color, #79a8ff); color: var(--hy-accent-color, #79a8ff); }
191
+ .filter-clear:disabled { cursor: default; opacity: 0.45; }
192
+ .empty { height: 112px; color: var(--hy-muted-text-color, #788697); text-align: center; }
193
+ .pagination {
194
+ display: flex;
195
+ align-items: center;
196
+ justify-content: flex-end;
197
+ gap: 6px;
198
+ padding: 11px 0 0;
199
+ }
200
+ .pagination[hidden] { display: none; }
201
+ .pagination button, .pagination select {
202
+ height: 30px;
203
+ box-sizing: border-box;
204
+ border: 1px solid var(--hy-border-color, #3d4654);
205
+ border-radius: 5px;
206
+ color: var(--hy-secondary-text-color, #aebbd0);
207
+ background: var(--hy-surface-color, #171d27);
208
+ font: 11px system-ui, sans-serif;
209
+ }
210
+ .pagination button { min-width: 30px; padding: 0 8px; cursor: pointer; }
211
+ .pagination button:hover:not(:disabled), .pagination button[aria-current="page"] { border-color: var(--hy-accent-color, #79a8ff); color: var(--hy-accent-color, #79a8ff); }
212
+ .pagination button:disabled { cursor: not-allowed; opacity: 0.45; }
213
+ .pagination select { margin-left: 6px; padding: 0 7px; }
214
+ .summary { margin-right: auto; color: var(--hy-muted-text-color, #788697); font-size: 10px; }
215
+ `;
216
+ this._scroller.className = 'scroller';
217
+ this._scroller.part.add('scroller');
218
+ this._table.part.add('table');
219
+ this._head.part.add('header');
220
+ this._body.part.add('body');
221
+ this._pagination.className = 'pagination';
222
+ this._pagination.part.add('pagination');
223
+ this._pagination.setAttribute('aria-label', 'Table pagination');
224
+ this._table.append(this._head, this._body);
225
+ this._scroller.append(this._table);
226
+ root.append(style, this._scroller, this._pagination);
227
+ }
228
+ connectedCallback() {
229
+ this._syncAttributes();
230
+ this._render();
231
+ }
232
+ attributeChangedCallback() {
233
+ if (this._reflecting)
234
+ return;
235
+ this._syncAttributes();
236
+ this._render();
237
+ }
238
+ get columns() { return this._columns; }
239
+ set columns(value) {
240
+ this._columns = Array.isArray(value) ? [...value] : [];
241
+ const keys = new Set(this._columns.map(column => column.key));
242
+ this._filters = Object.fromEntries(Object.entries(this._filters).filter(([key]) => keys.has(key)));
243
+ if (this._sort && !keys.has(this._sort.key))
244
+ this._sort = null;
245
+ this._clampPage();
246
+ this._render();
247
+ }
248
+ get dataSource() { return this._dataSource; }
249
+ set dataSource(value) {
250
+ this._dataSource = Array.isArray(value) ? [...value] : [];
251
+ this._clampPage();
252
+ this._renderBodyAndPagination();
253
+ }
254
+ get page() { return this._page; }
255
+ set page(value) { this.goToPage(value); }
256
+ get pageSize() { return this._pageSize; }
257
+ set pageSize(value) {
258
+ const next = finiteInteger(value, DEFAULT_PAGE_SIZE);
259
+ if (next === this._pageSize)
260
+ return;
261
+ this._pageSize = next;
262
+ this._page = 1;
263
+ this._reflectNumber('page-size', next);
264
+ this._reflectNumber('page', 1);
265
+ this._renderBodyAndPagination();
266
+ this._dispatchChange('paginate');
267
+ }
268
+ get pageSizeOptions() { return this._pageSizeOptions; }
269
+ set pageSizeOptions(value) {
270
+ const options = positiveIntegerOptions(Array.isArray(value) ? value : []);
271
+ this._pageSizeOptions = options.length ? options : DEFAULT_PAGE_SIZE_OPTIONS;
272
+ this.setAttribute('page-size-options', this._pageSizeOptions.join(','));
273
+ this._renderPagination();
274
+ }
275
+ get pagination() { return this.getAttribute('pagination') !== 'false'; }
276
+ set pagination(value) { this.setAttribute('pagination', String(Boolean(value))); }
277
+ get showFilters() { return this.getAttribute('show-filters') !== 'false'; }
278
+ set showFilters(value) { this.setAttribute('show-filters', String(Boolean(value))); }
279
+ get sort() { return this._sort ? { ...this._sort } : null; }
280
+ set sort(value) {
281
+ if (!value)
282
+ this._sort = null;
283
+ else {
284
+ const column = this._columns.find(item => item.key === value.key);
285
+ this._sort = column && isSortable(column) && (value.direction === 'asc' || value.direction === 'desc')
286
+ ? { key: value.key, direction: value.direction }
287
+ : null;
288
+ }
289
+ this._page = 1;
290
+ this._reflectNumber('page', 1);
291
+ this._render();
292
+ this._dispatchChange('sort');
293
+ }
294
+ get filters() { return { ...this._filters }; }
295
+ set filters(value) {
296
+ const allowed = new Set(this._columns.map(column => column.key));
297
+ this._filters = Object.fromEntries(Object.entries(value ?? {}).filter(([key, filter]) => allowed.has(key) && String(filter).length > 0).map(([key, filter]) => [key, String(filter)]));
298
+ this._page = 1;
299
+ this._reflectNumber('page', 1);
300
+ this._render();
301
+ this._dispatchChange('filter');
302
+ }
303
+ get filteredData() { return this._processedRecords().map(item => item.record); }
304
+ get visibleData() { return this._visibleRecords().map(item => item.record); }
305
+ goToPage(value) {
306
+ const next = Math.min(this.pageCount, finiteInteger(value, 1));
307
+ if (next === this._page)
308
+ return;
309
+ this._page = next;
310
+ this._reflectNumber('page', next);
311
+ this._renderBodyAndPagination();
312
+ this._dispatchChange('paginate');
313
+ }
314
+ get pageCount() {
315
+ if (!this.pagination)
316
+ return 1;
317
+ return Math.max(1, Math.ceil(this._processedRecords().length / this._pageSize));
318
+ }
319
+ sortBy(key, direction) {
320
+ const column = this._columns.find(item => item.key === key && isSortable(item));
321
+ if (!column)
322
+ return;
323
+ let nextDirection = direction;
324
+ if (!nextDirection) {
325
+ if (this._sort?.key !== key)
326
+ nextDirection = 'asc';
327
+ else if (this._sort.direction === 'asc')
328
+ nextDirection = 'desc';
329
+ else {
330
+ this.sort = null;
331
+ return;
332
+ }
333
+ }
334
+ this.sort = { key, direction: nextDirection };
335
+ }
336
+ clearSort() { this.sort = null; }
337
+ setFilter(key, value) {
338
+ const column = this._columns.find(item => item.key === key && isFilterable(item));
339
+ if (!column)
340
+ return;
341
+ const normalized = String(value);
342
+ if (normalized)
343
+ this._filters[key] = normalized;
344
+ else
345
+ delete this._filters[key];
346
+ this._page = 1;
347
+ this._reflectNumber('page', 1);
348
+ this._render();
349
+ this._dispatchChange('filter');
350
+ }
351
+ clearFilters() {
352
+ if (Object.keys(this._filters).length === 0)
353
+ return;
354
+ this._filters = {};
355
+ this._page = 1;
356
+ this._reflectNumber('page', 1);
357
+ this._render();
358
+ this._dispatchChange('filter');
359
+ }
360
+ _syncAttributes() {
361
+ this._pageSize = finiteInteger(this.getAttribute('page-size'), this._pageSize);
362
+ this._page = finiteInteger(this.getAttribute('page'), this._page);
363
+ if (this.hasAttribute('page-size-options')) {
364
+ const options = positiveIntegerOptions((this.getAttribute('page-size-options') ?? '').split(','));
365
+ if (options.length)
366
+ this._pageSizeOptions = [...new Set(options)].sort((a, b) => a - b);
367
+ }
368
+ const label = this.getAttribute('aria-label');
369
+ if (label)
370
+ this._table.setAttribute('aria-label', label);
371
+ else
372
+ this._table.removeAttribute('aria-label');
373
+ this._clampPage();
374
+ }
375
+ _render() {
376
+ this._renderHeader();
377
+ this._renderBodyAndPagination();
378
+ }
379
+ _renderHeader() {
380
+ const titleRow = document.createElement('tr');
381
+ titleRow.part.add('header-row');
382
+ for (const column of this._columns) {
383
+ const header = document.createElement('th');
384
+ header.scope = 'col';
385
+ header.part.add('header-cell', `header-cell-${partToken(column.key)}`);
386
+ this._applyColumnLayout(header, column);
387
+ const content = document.createElement('div');
388
+ content.className = 'header-content';
389
+ content.style.justifyContent = column.align === 'right' ? 'flex-end' : column.align === 'center' ? 'center' : 'flex-start';
390
+ if (isSortable(column)) {
391
+ const button = document.createElement('button');
392
+ button.type = 'button';
393
+ button.className = 'sort-button';
394
+ button.setAttribute('aria-label', `Sort by ${column.title}`);
395
+ const title = document.createElement('span');
396
+ title.className = 'header-title';
397
+ title.textContent = column.title;
398
+ const indicator = document.createElement('span');
399
+ indicator.className = 'sort-indicator';
400
+ const direction = this._sort?.key === column.key ? this._sort.direction : null;
401
+ indicator.textContent = direction === 'asc' ? '▲' : direction === 'desc' ? '▼' : '◆';
402
+ header.setAttribute('aria-sort', direction === 'asc' ? 'ascending' : direction === 'desc' ? 'descending' : 'none');
403
+ button.addEventListener('click', () => this.sortBy(column.key));
404
+ button.append(title, indicator);
405
+ content.append(button);
406
+ }
407
+ else {
408
+ const title = document.createElement('span');
409
+ title.className = 'header-title';
410
+ title.textContent = column.title;
411
+ content.append(title);
412
+ }
413
+ if (this.showFilters && column.showFilter !== false && isFilterable(column))
414
+ content.append(this._createFilterMenu(column));
415
+ header.append(content);
416
+ titleRow.append(header);
417
+ }
418
+ this._head.replaceChildren(titleRow);
419
+ }
420
+ _createFilterMenu(column) {
421
+ const menu = document.createElement('details');
422
+ menu.className = 'filter-menu';
423
+ menu.part.add('filter-menu');
424
+ const trigger = document.createElement('summary');
425
+ trigger.className = 'filter-trigger';
426
+ trigger.part.add('filter-trigger');
427
+ trigger.setAttribute('aria-label', `Filter ${column.title}`);
428
+ trigger.title = `Filter ${column.title}`;
429
+ trigger.dataset.active = String(Boolean(this._filters[column.key]));
430
+ const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
431
+ icon.setAttribute('viewBox', '0 0 16 16');
432
+ icon.setAttribute('aria-hidden', 'true');
433
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
434
+ path.setAttribute('d', 'M2 3h12L9.5 8v4l-3 1V8L2 3Z');
435
+ icon.append(path);
436
+ trigger.append(icon);
437
+ const panel = document.createElement('div');
438
+ panel.className = 'filter-panel';
439
+ panel.part.add('filter-panel');
440
+ let control;
441
+ if (column.filterOptions) {
442
+ const select = document.createElement('select');
443
+ const all = document.createElement('option');
444
+ all.value = '';
445
+ all.textContent = column.filterPlaceholder ?? 'All';
446
+ select.append(all, ...column.filterOptions.map(option => {
447
+ const element = document.createElement('option');
448
+ element.value = option.value;
449
+ element.textContent = option.label;
450
+ return element;
451
+ }));
452
+ control = select;
453
+ }
454
+ else {
455
+ const input = document.createElement('input');
456
+ input.type = 'search';
457
+ input.placeholder = column.filterPlaceholder ?? `Filter ${column.title}`;
458
+ control = input;
459
+ }
460
+ control.className = 'filter';
461
+ control.part.add('filter');
462
+ control.setAttribute('aria-label', `Filter ${column.title}`);
463
+ control.value = this._filters[column.key] ?? '';
464
+ const update = () => {
465
+ this._updateFilterFromControl(column.key, control.value);
466
+ trigger.dataset.active = String(Boolean(control.value));
467
+ clear.disabled = !control.value;
468
+ };
469
+ control.addEventListener(control instanceof HTMLSelectElement ? 'change' : 'input', update);
470
+ control.addEventListener('keydown', event => {
471
+ if (!(event instanceof KeyboardEvent) || event.key !== 'Escape')
472
+ return;
473
+ menu.open = false;
474
+ trigger.focus();
475
+ });
476
+ const clear = document.createElement('button');
477
+ clear.type = 'button';
478
+ clear.className = 'filter-clear';
479
+ clear.part.add('filter-clear');
480
+ clear.textContent = 'Clear';
481
+ clear.disabled = !control.value;
482
+ clear.addEventListener('click', () => {
483
+ control.value = '';
484
+ update();
485
+ control.focus();
486
+ });
487
+ menu.addEventListener('toggle', () => {
488
+ if (!menu.open)
489
+ return;
490
+ for (const sibling of this._head.querySelectorAll('.filter-menu[open]')) {
491
+ if (sibling !== menu)
492
+ sibling.open = false;
493
+ }
494
+ control.focus();
495
+ });
496
+ panel.append(control, clear);
497
+ menu.append(trigger, panel);
498
+ return menu;
499
+ }
500
+ _updateFilterFromControl(key, value) {
501
+ if (value)
502
+ this._filters[key] = value;
503
+ else
504
+ delete this._filters[key];
505
+ this._page = 1;
506
+ this._reflectNumber('page', 1);
507
+ this._renderBodyAndPagination();
508
+ this._dispatchChange('filter');
509
+ }
510
+ _renderBodyAndPagination() {
511
+ this._clampPage();
512
+ const rows = this._visibleRecords();
513
+ if (rows.length === 0) {
514
+ const row = document.createElement('tr');
515
+ row.part.add('empty-row');
516
+ const cell = document.createElement('td');
517
+ cell.className = 'empty';
518
+ cell.part.add('empty');
519
+ cell.colSpan = Math.max(1, this._columns.length);
520
+ const slot = document.createElement('slot');
521
+ slot.name = 'empty';
522
+ slot.textContent = 'No data';
523
+ cell.append(slot);
524
+ row.append(cell);
525
+ this._body.replaceChildren(row);
526
+ }
527
+ else {
528
+ this._body.replaceChildren(...rows.map(item => this._createRow(item)));
529
+ }
530
+ this._renderPagination();
531
+ }
532
+ _createRow(item) {
533
+ const row = document.createElement('tr');
534
+ row.part.add('row');
535
+ row.dataset.index = String(item.index);
536
+ row.addEventListener('click', () => {
537
+ this.dispatchEvent(new CustomEvent('row-click', {
538
+ bubbles: true, composed: true, detail: { record: item.record, index: item.index },
539
+ }));
540
+ });
541
+ for (const column of this._columns) {
542
+ const cell = document.createElement('td');
543
+ cell.part.add('cell', `cell-${partToken(column.key)}`);
544
+ this._applyColumnLayout(cell, column);
545
+ const value = readValue(item.record, column.dataIndex ?? column.key);
546
+ const content = column.render ? column.render(value, item.record, item.index) : value;
547
+ if (content instanceof Node)
548
+ cell.append(content);
549
+ else
550
+ cell.textContent = content == null ? '' : String(content);
551
+ row.append(cell);
552
+ }
553
+ return row;
554
+ }
555
+ _renderPagination() {
556
+ if (!this.pagination) {
557
+ this._pagination.hidden = true;
558
+ this._pagination.replaceChildren();
559
+ return;
560
+ }
561
+ this._pagination.hidden = false;
562
+ const processed = this._processedRecords();
563
+ const pageCount = Math.max(1, Math.ceil(processed.length / this._pageSize));
564
+ const summary = document.createElement('span');
565
+ summary.className = 'summary';
566
+ summary.part.add('summary');
567
+ const start = processed.length ? (this._page - 1) * this._pageSize + 1 : 0;
568
+ const end = Math.min(processed.length, this._page * this._pageSize);
569
+ summary.textContent = `${start}–${end} of ${processed.length}`;
570
+ const previous = this._pageButton('‹', 'Previous page', this._page - 1, this._page <= 1);
571
+ const pages = this._pageNumbers(pageCount).map(page => this._pageButton(String(page), `Page ${page}`, page, false, page === this._page));
572
+ const next = this._pageButton('›', 'Next page', this._page + 1, this._page >= pageCount);
573
+ const pageSize = document.createElement('select');
574
+ pageSize.part.add('page-size');
575
+ pageSize.setAttribute('aria-label', 'Rows per page');
576
+ const options = this._pageSizeOptions.includes(this._pageSize)
577
+ ? this._pageSizeOptions
578
+ : [...this._pageSizeOptions, this._pageSize].sort((a, b) => a - b);
579
+ pageSize.append(...options.map(value => {
580
+ const option = document.createElement('option');
581
+ option.value = String(value);
582
+ option.textContent = `${value} / page`;
583
+ option.selected = value === this._pageSize;
584
+ return option;
585
+ }));
586
+ pageSize.addEventListener('change', () => { this.pageSize = Number(pageSize.value); });
587
+ this._pagination.replaceChildren(summary, previous, ...pages, next, pageSize);
588
+ }
589
+ _pageButton(text, label, page, disabled, current = false) {
590
+ const button = document.createElement('button');
591
+ button.type = 'button';
592
+ button.textContent = text;
593
+ button.disabled = disabled;
594
+ button.setAttribute('aria-label', label);
595
+ if (current)
596
+ button.setAttribute('aria-current', 'page');
597
+ button.addEventListener('click', () => this.goToPage(page));
598
+ return button;
599
+ }
600
+ _pageNumbers(pageCount) {
601
+ if (pageCount <= 7)
602
+ return Array.from({ length: pageCount }, (_, index) => index + 1);
603
+ const start = Math.max(1, Math.min(pageCount - 4, this._page - 2));
604
+ return Array.from({ length: 5 }, (_, index) => start + index);
605
+ }
606
+ _processedRecords() {
607
+ let records = this._dataSource.map((record, index) => ({ record, index }));
608
+ for (const [key, filterValue] of Object.entries(this._filters)) {
609
+ const column = this._columns.find(item => item.key === key);
610
+ if (!column || !filterValue)
611
+ continue;
612
+ const normalized = filterValue.toLocaleLowerCase();
613
+ records = records.filter(item => {
614
+ if (column.filter)
615
+ return column.filter(filterValue, item.record);
616
+ const value = String(readValue(item.record, column.dataIndex ?? column.key) ?? '');
617
+ return column.filterOptions
618
+ ? value === filterValue
619
+ : value.toLocaleLowerCase().includes(normalized);
620
+ });
621
+ }
622
+ if (this._sort) {
623
+ const column = this._columns.find(item => item.key === this._sort?.key);
624
+ if (column) {
625
+ const direction = this._sort.direction === 'asc' ? 1 : -1;
626
+ records.sort((left, right) => {
627
+ const compared = column.sorter
628
+ ? column.sorter(left.record, right.record)
629
+ : defaultCompare(readValue(left.record, column.dataIndex ?? column.key), readValue(right.record, column.dataIndex ?? column.key));
630
+ return compared === 0 ? left.index - right.index : compared * direction;
631
+ });
632
+ }
633
+ }
634
+ return records;
635
+ }
636
+ _visibleRecords() {
637
+ const records = this._processedRecords();
638
+ if (!this.pagination)
639
+ return records;
640
+ const start = (this._page - 1) * this._pageSize;
641
+ return records.slice(start, start + this._pageSize);
642
+ }
643
+ _clampPage() {
644
+ const total = this._processedRecords().length;
645
+ const pageCount = this.pagination ? Math.max(1, Math.ceil(total / this._pageSize)) : 1;
646
+ const next = Math.min(pageCount, Math.max(1, this._page));
647
+ if (next !== this._page) {
648
+ this._page = next;
649
+ this._reflectNumber('page', next);
650
+ }
651
+ }
652
+ _applyColumnLayout(element, column) {
653
+ element.style.textAlign = column.align ?? 'left';
654
+ if (typeof column.width === 'number')
655
+ element.style.width = `${column.width}px`;
656
+ else if (column.width)
657
+ element.style.width = column.width;
658
+ }
659
+ _reflectNumber(name, value) {
660
+ const text = String(value);
661
+ if (this.getAttribute(name) === text)
662
+ return;
663
+ this._reflecting = true;
664
+ this.setAttribute(name, text);
665
+ this._reflecting = false;
666
+ }
667
+ _dispatchChange(reason) {
668
+ const processed = this._processedRecords();
669
+ const detail = {
670
+ reason,
671
+ page: this._page,
672
+ pageSize: this._pageSize,
673
+ pageCount: this.pagination ? Math.max(1, Math.ceil(processed.length / this._pageSize)) : 1,
674
+ total: this._dataSource.length,
675
+ filteredTotal: processed.length,
676
+ sort: this.sort,
677
+ filters: this.filters,
678
+ visibleData: this.visibleData,
679
+ };
680
+ this.dispatchEvent(new CustomEvent('table-change', {
681
+ bubbles: true, composed: true, detail,
682
+ }));
683
+ }
684
+ }
685
+ export function defineTableComponents() {
686
+ if (!customElements.get('hy-table'))
687
+ customElements.define('hy-table', HYTable);
688
+ }
689
+ //# sourceMappingURL=table.js.map