@excom/data-table 0.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 (38) hide show
  1. package/.rush/temp/chunked-rush-logs/data-table.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/data-table.build_docs.chunks.jsonl +1 -0
  3. package/.rush/temp/chunked-rush-logs/data-table.build_package-metas.chunks.jsonl +1 -0
  4. package/.rush/temp/operation/apply-exports/all.log +1 -0
  5. package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
  6. package/.rush/temp/operation/apply-exports/state.json +3 -0
  7. package/.rush/temp/operation/build_docs/all.log +1 -0
  8. package/.rush/temp/operation/build_docs/log-chunks.jsonl +1 -0
  9. package/.rush/temp/operation/build_docs/state.json +3 -0
  10. package/.rush/temp/operation/build_package-metas/all.log +1 -0
  11. package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
  12. package/.rush/temp/operation/build_package-metas/state.json +3 -0
  13. package/.rush/temp/shrinkwrap-deps.json +3 -0
  14. package/config/rig.json +5 -0
  15. package/data-table.ts +396 -0
  16. package/data-th.ts +49 -0
  17. package/index.css +5 -0
  18. package/index.ts +23 -0
  19. package/package.json +44 -0
  20. package/rush-logs/data-table.apply-exports.cache.log +1 -0
  21. package/rush-logs/data-table.apply-exports.log +1 -0
  22. package/rush-logs/data-table.build_docs.cache.log +1 -0
  23. package/rush-logs/data-table.build_docs.log +1 -0
  24. package/rush-logs/data-table.build_package-metas.cache.log +1 -0
  25. package/rush-logs/data-table.build_package-metas.log +1 -0
  26. package/src/data-table.css +124 -0
  27. package/support/custom-elements.json +366 -0
  28. package/support/demos/filter.html +67 -0
  29. package/support/demos/simple.html +14 -0
  30. package/support/dist-docs/data-table.md +245 -0
  31. package/support/dist-docs/data-th.md +29 -0
  32. package/support/docs/INTERNAL.md +7 -0
  33. package/support/docs/README.md +74 -0
  34. package/support/package-meta.json +280 -0
  35. package/support/tests/data-table.test.ts +704 -0
  36. package/support/tests/filter.view.test.ts +33 -0
  37. package/support/tests/simple.view.test.ts +31 -0
  38. package/tsconfig.json +5 -0
@@ -0,0 +1,124 @@
1
+ @import "@excom/valence/src/content/table.css";
2
+
3
+ /**
4
+ * Styles for `<data-table>` and the plain structural tags that make up a
5
+ * table: `<data-thead>`, `<data-tbody>`, `<data-tr>`, `<data-th>`,
6
+ * `<data-td>`, `<data-tfoot>`, `<data-tf>`. Only `<data-table>` and
7
+ * `<data-th>` are registered Neutron elements — the rest are inert custom
8
+ * tags styled here so markup reads like a real `<table>`. Use the
9
+ * `.tag-data-*` classes to apply the same styling to other elements
10
+ * (e.g. a real `<table>` / `<tr>` / `<td>`).
11
+ * @element data-table
12
+ */
13
+
14
+ /* Aliases */
15
+ @custom-selector :--data-table data-table, .tag-data-table;
16
+ @custom-selector :--data-thead data-thead, .tag-data-thead;
17
+ @custom-selector :--data-tbody data-tbody, .tag-data-tbody;
18
+ @custom-selector :--data-tfoot data-tfoot, .tag-data-tfoot;
19
+ @custom-selector :--data-tr data-tr, .tag-data-tr;
20
+ @custom-selector :--data-th data-th, .tag-data-th;
21
+ @custom-selector :--data-td data-td, .tag-data-td;
22
+ @custom-selector :--data-tf data-tf, .tag-data-tf;
23
+ /* NOOP: we treat every `<data-th>` as sortable */
24
+ @custom-selector :--data-th--sort :is(:--data-th);
25
+ @custom-selector :--data-th--sort-asc [sort-direction="asc"], [aria-sort="ascending"];
26
+ @custom-selector :--data-th--sort-desc [sort-direction="desc"], [aria-sort="descending"];
27
+
28
+ @define-mixin data-table-detect-cols $n {
29
+ &:has(:--data-tr > :nth-child($(n))) {
30
+ --data-table-cols-detected: $(n);
31
+ }
32
+ }
33
+
34
+ @define-mixin module-data-table {
35
+ @mixin module-table :--data-table, :--data-thead, :--data-tbody,
36
+ :--data-tfoot, :--data-tr, :--data-th, :--data-td, :--data-th--sort,
37
+ :--data-th--sort-asc, :--data-th--sort-desc;
38
+ :--data-table {
39
+ display: grid;
40
+ /**
41
+ * Column count for shared subgrid tracks. Auto-detected from the
42
+ * widest row; set on the host to override.
43
+ * @cssproperty --data-table-cols
44
+ * @syntax <integer>
45
+ */
46
+ --data-table-cols-detected: 1;
47
+ /* Leading columns hug content; last takes leftover so the table
48
+ stays `width: 100%`. `1fr` on every track would equalize. */
49
+ grid-template-columns:
50
+ repeat(
51
+ calc(var(--data-table-cols, var(--data-table-cols-detected)) - 1),
52
+ auto
53
+ )
54
+ minmax(min-content, 1fr);
55
+
56
+ /* Widest row wins (later rules override). Author `--data-table-cols` wins. */
57
+ @mixin data-table-detect-cols 2;
58
+ @mixin data-table-detect-cols 3;
59
+ @mixin data-table-detect-cols 4;
60
+ @mixin data-table-detect-cols 5;
61
+ @mixin data-table-detect-cols 6;
62
+ @mixin data-table-detect-cols 7;
63
+ @mixin data-table-detect-cols 8;
64
+ @mixin data-table-detect-cols 9;
65
+ @mixin data-table-detect-cols 10;
66
+ @mixin data-table-detect-cols 11;
67
+ @mixin data-table-detect-cols 12;
68
+ @mixin data-table-detect-cols 13;
69
+ @mixin data-table-detect-cols 14;
70
+ @mixin data-table-detect-cols 15;
71
+ @mixin data-table-detect-cols 16;
72
+
73
+ :--data-thead > :--data-tr {
74
+ order: 0;
75
+ }
76
+ :--data-tbody > :--data-tr {
77
+ /**
78
+ * Visual row order (1-based). Set by `<data-table>` on sort —
79
+ * not meant to be set by hand.
80
+ * @cssproperty
81
+ * @syntax <integer>
82
+ */
83
+ order: var(--data-tr-order);
84
+ }
85
+ :--data-tfoot > :--data-tr {
86
+ order: 9999;
87
+ }
88
+
89
+ :--data-thead,
90
+ :--data-tbody,
91
+ :--data-tfoot {
92
+ display: contents;
93
+ }
94
+ :--data-tr {
95
+ /**
96
+ * Row display. `<data-table>` sets `none` to hide filtered-out
97
+ * rows — not meant to be set by hand.
98
+ * @cssproperty
99
+ * @syntax none | grid
100
+ * @default grid
101
+ */
102
+ display: var(--data-tr-display, grid);
103
+ grid-column: 1 / -1;
104
+ grid-template-columns: subgrid;
105
+ :--data-th,
106
+ :--data-td,
107
+ :--data-tf {
108
+ display: block;
109
+ /* Borders via `:nth-of-type`; `order` only changes visual layout */
110
+ border-bottom: var(--v-border-width) solid var(--v-table-border-color) !important;
111
+ border-radius: 0 !important;
112
+ }
113
+ }
114
+ &:not(:has(:--data-tbody :--data-tr + :--data-tr)) {
115
+ /* One row or none: don't look sortable */
116
+ :--data-th {
117
+ cursor: default;
118
+ &::after {
119
+ content: "";
120
+ }
121
+ }
122
+ }
123
+ }
124
+ }
@@ -0,0 +1,366 @@
1
+ {
2
+ "schemaVersion": "1.0.0",
3
+ "modules": [
4
+ {
5
+ "kind": "javascript-module",
6
+ "path": "data-table.ts",
7
+ "declarations": [
8
+ {
9
+ "kind": "class",
10
+ "name": "DataTable",
11
+ "customElement": true,
12
+ "tagName": "data-table",
13
+ "summary": "Sortable / filterable data table — plain tags, one behavior owner.",
14
+ "description": "Options the `--export` invoker carries as `data-*` attributes (`data-file-type=\"json\"`, `data-file-name=\"people\"`, `data-full`).",
15
+ "attributes": [
16
+ {
17
+ "name": "filter-value",
18
+ "type": {
19
+ "text": "string"
20
+ },
21
+ "description": "Hides `data-tr` rows (via `--data-tr-display: none`) whose text content doesn't include this value. Case-insensitive unless `filter-casing` is set. Unset / empty clears the filter. Rows stay in the DOM so Quark bindings survive.",
22
+ "fieldName": "filterValue"
23
+ },
24
+ {
25
+ "name": "filter-casing",
26
+ "type": {
27
+ "text": "boolean"
28
+ },
29
+ "description": "Match `filter-value` case-sensitively instead of the default case-insensitive comparison.",
30
+ "fieldName": "filterCasing"
31
+ }
32
+ ],
33
+ "members": [
34
+ {
35
+ "kind": "field",
36
+ "name": "filterValue",
37
+ "type": {
38
+ "text": "string"
39
+ },
40
+ "privacy": "public",
41
+ "readonly": false,
42
+ "description": "Hides `data-tr` rows (via `--data-tr-display: none`) whose text content doesn't include this value. Case-insensitive unless `filter-casing` is set. Unset / empty clears the filter. Rows stay in the DOM so Quark bindings survive.",
43
+ "_neutron": {
44
+ "surface": "option"
45
+ }
46
+ },
47
+ {
48
+ "kind": "field",
49
+ "name": "filterCasing",
50
+ "type": {
51
+ "text": "boolean"
52
+ },
53
+ "privacy": "public",
54
+ "readonly": false,
55
+ "description": "Match `filter-value` case-sensitively instead of the default case-insensitive comparison.",
56
+ "_neutron": {
57
+ "surface": "option"
58
+ }
59
+ },
60
+ {
61
+ "kind": "field",
62
+ "name": "provision",
63
+ "type": {
64
+ "text": "DataTableProvision",
65
+ "expanded": "{ totalRows: number; visibleRows: number; sortColumnIndex: number | null; sortDirection: \"asc\" | \"desc\" | null; filterValue: string | null; }"
66
+ },
67
+ "privacy": "public",
68
+ "readonly": false,
69
+ "description": "`{ totalRows, visibleRows, sortColumnIndex, sortDirection, filterValue }` — recomputed after connect, after the `data-table-sort` default action, and after every filter change. Not reflected as an attribute.",
70
+ "_neutron": {
71
+ "surface": "option"
72
+ }
73
+ }
74
+ ],
75
+ "events": [
76
+ {
77
+ "name": "data-table-sort",
78
+ "description": "Cancelable. Dispatched when the active sort column / direction changes: on connect (if a `data-th` already has `sort-direction`) and after every `data-th-sort`. Call `preventDefault()` to take over sorting yourself.",
79
+ "type": {
80
+ "text": "DataTableSortEvent",
81
+ "expanded": "CustomEvent & { type: \"data-table-sort\"; detail: { sortDirection: \"asc\" | \"desc\"; columnType: \"string\" | \"number\" | \"date\"; columnIndex: number; sortFn: (a: string, b: string) => number; rows: HTMLElement[]; }; bubbles: true; cancelable: true; composed: true }"
82
+ }
83
+ }
84
+ ],
85
+ "_neutron": {
86
+ "provisions": [
87
+ {
88
+ "name": "provision",
89
+ "type": {
90
+ "text": "DataTableProvision",
91
+ "expanded": "{ totalRows: number; visibleRows: number; sortColumnIndex: number | null; sortDirection: \"asc\" | \"desc\" | null; filterValue: string | null; }"
92
+ },
93
+ "description": "`{ totalRows, visibleRows, sortColumnIndex, sortDirection, filterValue }` — recomputed after connect, after the `data-table-sort` default action, and after every filter change. Not reflected as an attribute.",
94
+ "fieldName": "provision"
95
+ }
96
+ ],
97
+ "listens": [
98
+ {
99
+ "name": "data-th-sort",
100
+ "description": "Bubbled up from a descendant `data-th` when its `sort-direction` changes. Sets that header as the active sort column (clearing `sort-direction` from the previously active one) and emits `data-table-sort`.",
101
+ "type": {
102
+ "text": "DataThSortEvent",
103
+ "expanded": "CustomEvent & { type: \"data-th-sort\"; detail: void; bubbles: true; cancelable: true; composed: true }"
104
+ }
105
+ }
106
+ ],
107
+ "commands": [
108
+ {
109
+ "name": "--export",
110
+ "description": "Builds a file from the table's `data-tr` / `data-td` / `data-th` text content and downloads it. Options are `data-*` on the invoker: `data-file-type` (`csv`, the default, or `json`), `data-file-name` (default `export_table_<locale-date>`), and `data-full` to download every row in DOM order instead of only the visible rows in visual sort order. Filtering needs no command: write `filter-value` / `filter-casing`."
111
+ }
112
+ ],
113
+ "defaultActions": [
114
+ {
115
+ "name": "data-table-sort",
116
+ "description": "Sorts `event.detail.rows` by `columnIndex` with `sortFn` and sets `--data-tr-order` on each row (visual CSS `order` — DOM order is unchanged)."
117
+ }
118
+ ],
119
+ "expectedChildren": [
120
+ {
121
+ "relationship": "descendant",
122
+ "selector": "data-th",
123
+ "required": true,
124
+ "description": "Sortable column header. Clicking toggles its `sort-direction` and fires `data-th-sort`, which becomes the active column."
125
+ },
126
+ {
127
+ "relationship": "descendant",
128
+ "selector": "data-tbody",
129
+ "required": true,
130
+ "description": "Required row container. Sorting and filtering both operate on its `data-tr` children."
131
+ },
132
+ {
133
+ "relationship": "descendant",
134
+ "selector": "data-tr",
135
+ "required": true,
136
+ "description": "Row, direct child of `data-tbody`. Gets `--data-tr-order` on sort and `--data-tr-display` on filter. DOM order is unchanged."
137
+ },
138
+ {
139
+ "relationship": "descendant",
140
+ "selector": "data-td",
141
+ "required": true,
142
+ "description": "Cell within a `data-tr`, read as sortable / export cell content."
143
+ }
144
+ ],
145
+ "cssAliases": [
146
+ {
147
+ "name": ":--data-table",
148
+ "selectors": [
149
+ "data-table",
150
+ ".tag-data-table"
151
+ ],
152
+ "kind": "element"
153
+ },
154
+ {
155
+ "name": ":--data-thead",
156
+ "selectors": [
157
+ "data-thead",
158
+ ".tag-data-thead"
159
+ ],
160
+ "kind": "element"
161
+ },
162
+ {
163
+ "name": ":--data-tbody",
164
+ "selectors": [
165
+ "data-tbody",
166
+ ".tag-data-tbody"
167
+ ],
168
+ "kind": "element"
169
+ },
170
+ {
171
+ "name": ":--data-tfoot",
172
+ "selectors": [
173
+ "data-tfoot",
174
+ ".tag-data-tfoot"
175
+ ],
176
+ "kind": "element"
177
+ },
178
+ {
179
+ "name": ":--data-tr",
180
+ "selectors": [
181
+ "data-tr",
182
+ ".tag-data-tr"
183
+ ],
184
+ "kind": "element"
185
+ },
186
+ {
187
+ "name": ":--data-td",
188
+ "selectors": [
189
+ "data-td",
190
+ ".tag-data-td"
191
+ ],
192
+ "kind": "element"
193
+ },
194
+ {
195
+ "name": ":--data-tf",
196
+ "selectors": [
197
+ "data-tf",
198
+ ".tag-data-tf"
199
+ ],
200
+ "kind": "element"
201
+ }
202
+ ]
203
+ },
204
+ "cssProperties": [
205
+ {
206
+ "name": "--data-table-cols",
207
+ "description": "Column count for shared subgrid tracks. Auto-detected from the widest row; set on the host to override.",
208
+ "syntax": "<integer>",
209
+ "default": "1"
210
+ }
211
+ ]
212
+ }
213
+ ],
214
+ "exports": [
215
+ {
216
+ "kind": "js",
217
+ "name": "DataTable",
218
+ "declaration": {
219
+ "name": "DataTable",
220
+ "module": "data-table.ts"
221
+ }
222
+ },
223
+ {
224
+ "kind": "custom-element-definition",
225
+ "name": "data-table",
226
+ "declaration": {
227
+ "name": "DataTable",
228
+ "module": "data-table.ts"
229
+ }
230
+ }
231
+ ]
232
+ },
233
+ {
234
+ "kind": "javascript-module",
235
+ "path": "data-th.ts",
236
+ "declarations": [
237
+ {
238
+ "kind": "class",
239
+ "name": "DataTh",
240
+ "customElement": true,
241
+ "tagName": "data-th",
242
+ "summary": "Sortable column header — click toggles sort direction.",
243
+ "description": "Sortable column header. Clicking toggles `sort-direction` between `asc` / `desc`; the parent `<data-table>` listens for the resulting `data-th-sort` event to drive sorting and to track which header is active. Renders no chrome of its own — style via `.tag-data-th` or the sort-direction indicators in `data-table`'s stylesheet.",
244
+ "attributes": [
245
+ {
246
+ "name": "column-type",
247
+ "type": {
248
+ "text": "string"
249
+ },
250
+ "description": "Sort comparator to use for this column's cell values. `data-table` treats `null` as \"string\".",
251
+ "fieldName": "columnType",
252
+ "values": [
253
+ "string",
254
+ "number",
255
+ "date"
256
+ ]
257
+ },
258
+ {
259
+ "name": "sort-direction",
260
+ "type": {
261
+ "text": "string"
262
+ },
263
+ "description": "Current sort direction. A click toggles between `asc` / `desc`; setting it (by any means) fires `data-th-sort`. Only one `data-th` per table should carry this at a time — the parent `<data-table>` clears the previously active header when a new one is set.",
264
+ "fieldName": "sortDirection",
265
+ "values": [
266
+ "asc",
267
+ "desc"
268
+ ]
269
+ }
270
+ ],
271
+ "members": [
272
+ {
273
+ "kind": "field",
274
+ "name": "columnType",
275
+ "type": {
276
+ "text": "string"
277
+ },
278
+ "privacy": "public",
279
+ "readonly": false,
280
+ "description": "Sort comparator to use for this column's cell values. `data-table` treats `null` as \"string\".",
281
+ "_neutron": {
282
+ "surface": "option"
283
+ }
284
+ },
285
+ {
286
+ "kind": "field",
287
+ "name": "sortDirection",
288
+ "type": {
289
+ "text": "string"
290
+ },
291
+ "privacy": "public",
292
+ "readonly": false,
293
+ "description": "Current sort direction. A click toggles between `asc` / `desc`; setting it (by any means) fires `data-th-sort`. Only one `data-th` per table should carry this at a time — the parent `<data-table>` clears the previously active header when a new one is set.",
294
+ "_neutron": {
295
+ "surface": "hybrid"
296
+ }
297
+ }
298
+ ],
299
+ "events": [
300
+ {
301
+ "name": "data-th-sort",
302
+ "description": "Dispatched whenever `sort-direction` is set, whether by a click or programmatically. Bubbles to the parent `<data-table>`.",
303
+ "type": {
304
+ "text": "DataThSortEvent",
305
+ "expanded": "CustomEvent & { type: \"data-th-sort\"; detail: void; bubbles: true; cancelable: true; composed: true }"
306
+ }
307
+ }
308
+ ],
309
+ "_neutron": {
310
+ "cssAliases": [
311
+ {
312
+ "name": ":--data-th",
313
+ "selectors": [
314
+ "data-th",
315
+ ".tag-data-th"
316
+ ],
317
+ "kind": "element"
318
+ },
319
+ {
320
+ "name": ":--data-th--sort",
321
+ "selectors": [
322
+ ":is(:--data-th)"
323
+ ],
324
+ "kind": "state"
325
+ },
326
+ {
327
+ "name": ":--data-th--sort-asc",
328
+ "selectors": [
329
+ "[sort-direction=\"asc\"]",
330
+ "[aria-sort=\"ascending\"]"
331
+ ],
332
+ "kind": "state"
333
+ },
334
+ {
335
+ "name": ":--data-th--sort-desc",
336
+ "selectors": [
337
+ "[sort-direction=\"desc\"]",
338
+ "[aria-sort=\"descending\"]"
339
+ ],
340
+ "kind": "state"
341
+ }
342
+ ]
343
+ }
344
+ }
345
+ ],
346
+ "exports": [
347
+ {
348
+ "kind": "js",
349
+ "name": "DataTh",
350
+ "declaration": {
351
+ "name": "DataTh",
352
+ "module": "data-th.ts"
353
+ }
354
+ },
355
+ {
356
+ "kind": "custom-element-definition",
357
+ "name": "data-th",
358
+ "declaration": {
359
+ "name": "DataTh",
360
+ "module": "data-th.ts"
361
+ }
362
+ }
363
+ ]
364
+ }
365
+ ]
366
+ }
@@ -0,0 +1,67 @@
1
+ <div>
2
+ <quark-sheet>
3
+ :scope {
4
+ @on input (target: "form[data-filter]") {
5
+ data-table { filter-value: target.elements.filterValue.value; }
6
+ }
7
+ @on change (target: "form[data-filter]") {
8
+ data-table { filter-casing: target.elements.filterCasing.checked; }
9
+ }
10
+ @on change (target: "form[data-export]") {
11
+ form[data-export] [command="--export"] {
12
+ data-file-type: target.elements.fileType.value;
13
+ data-file-name: target.elements.fileName.value;
14
+ data-full: target.elements.full.checked;
15
+ }
16
+ }
17
+ }
18
+ </quark-sheet>
19
+ <form data-filter>
20
+ <fieldset role="group">
21
+ <input name="filterValue" placeholder="Filter table…" />
22
+ <label>
23
+ Case-sensitive
24
+ <input name="filterCasing" role="switch" type="checkbox" />
25
+ </label>
26
+ </fieldset>
27
+ </form>
28
+ <data-table id="demo-data-table-filter">
29
+ <data-thead>
30
+ <data-tr>
31
+ <data-th column-type="string">Name</data-th>
32
+ <data-th column-type="number">Age</data-th>
33
+ </data-tr>
34
+ </data-thead>
35
+ <data-tbody>
36
+ <data-tr><data-td>Beatrice</data-td><data-td>44</data-td></data-tr>
37
+ <data-tr><data-td>Cynthia</data-td><data-td>41</data-td></data-tr>
38
+ <data-tr><data-td>Beau</data-td><data-td>29</data-td></data-tr>
39
+ <data-tr><data-td>Adam</data-td><data-td>36</data-td></data-tr>
40
+ </data-tbody>
41
+ </data-table>
42
+ <hr />
43
+ <button type="button" command="--export" commandfor="demo-data-table-filter">
44
+ Export as-is
45
+ </button>
46
+ <hr />
47
+ <form data-export class="tag-article">
48
+ <header>
49
+ <h4>Export with options</h4>
50
+ </header>
51
+ <label>
52
+ <select name="fileType">
53
+ <option value="csv" selected>CSV</option>
54
+ <option value="json">JSON</option>
55
+ </select>
56
+ </label>
57
+ <label>
58
+ File name
59
+ <input name="fileName" placeholder="File name" />
60
+ </label>
61
+ <label>
62
+ <input name="full" type="checkbox" role="switch" />
63
+ Full table (ignores active filter/sort)
64
+ </label>
65
+ <button type="button" command="--export" commandfor="demo-data-table-filter">Export</button>
66
+ </form>
67
+ </div>
@@ -0,0 +1,14 @@
1
+ <data-table>
2
+ <data-thead>
3
+ <data-tr>
4
+ <data-th column-type="string" sort-direction="asc">Name</data-th>
5
+ <data-th column-type="number">Age</data-th>
6
+ </data-tr>
7
+ </data-thead>
8
+ <data-tbody>
9
+ <data-tr><data-td>Beatrice</data-td><data-td>44</data-td></data-tr>
10
+ <data-tr><data-td>Cynthia</data-td><data-td>41</data-td></data-tr>
11
+ <data-tr><data-td>Beau</data-td><data-td>29</data-td></data-tr>
12
+ <data-tr><data-td>Adam</data-td><data-td>36</data-td></data-tr>
13
+ </data-tbody>
14
+ </data-table>