@birdapi/velinstyle 0.9.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 (51) hide show
  1. package/README.de.md +15 -11
  2. package/README.md +15 -11
  3. package/cli/cli-manifest.json +1 -1
  4. package/cli/docgen/extract-a11y.js +4 -2
  5. package/cli/index.js +4 -2
  6. package/components/index.js +2 -0
  7. package/components/runtime/component-loaders.js +2 -0
  8. package/components/velin-data-table.js +361 -0
  9. package/components/velin-form-summary.js +348 -0
  10. package/core/a11y/component-contracts.json +18 -1
  11. package/core/highlight/languages/go.js +28 -0
  12. package/core/highlight/languages/python.js +29 -0
  13. package/core/highlight/languages/rust.js +32 -0
  14. package/core/highlight/languages/yaml.js +23 -0
  15. package/core/highlight/registry.js +13 -2
  16. package/core/meta/build.js +1 -1
  17. package/core/search/worker-client.js +2 -3
  18. package/dist/chunks/attributes-353XDOAX.js +391 -0
  19. package/dist/chunks/attributes-ADOKLKA7.js +391 -0
  20. package/dist/chunks/chunk-ERF5YVP4.js +253 -0
  21. package/dist/chunks/chunk-IEHKRARD.js +109 -0
  22. package/dist/chunks/chunk-QVHYL3R4.js +111 -0
  23. package/dist/chunks/go-HZ6XTFCK.js +31 -0
  24. package/dist/chunks/highlight-IBDX4XWS.js +37 -0
  25. package/dist/chunks/python-BMPKDKNO.js +32 -0
  26. package/dist/chunks/runtime-entry.js +1 -1
  27. package/dist/chunks/rust-2BWKI2MN.js +35 -0
  28. package/dist/chunks/velin-code-block-ITCLIC6T.js +132 -0
  29. package/dist/chunks/velin-data-table-KK7IDGTP.js +302 -0
  30. package/dist/chunks/velin-form-summary-XRQ7COQH.js +273 -0
  31. package/dist/chunks/velin-lightbox-NXA3U2FM.js +149 -0
  32. package/dist/chunks/velin-search-FDHEMCSO.js +557 -0
  33. package/dist/chunks/worker-client-MPT7PNQ4.js +47 -0
  34. package/dist/chunks/yaml-O67LEQYT.js +26 -0
  35. package/dist/llms.txt +2 -2
  36. package/dist/search-index.json +28 -2
  37. package/dist/velin-agent.json +31 -5
  38. package/dist/velinstyle-components.iife.js +745 -7
  39. package/dist/velinstyle-components.js +746 -5
  40. package/dist/velinstyle-components.min.js +85 -85
  41. package/dist/velinstyle.css +141 -19
  42. package/dist/velinstyle.d.ts +2 -0
  43. package/dist/velinstyle.min.css +1 -1
  44. package/package.json +8 -2
  45. package/src/base/wc-placeholder.css +7 -0
  46. package/src/components/data-table.css +93 -0
  47. package/src/components/form-validation.css +40 -0
  48. package/src/velinstyle.css +1 -0
  49. package/dist/llms.test.txt +0 -40
  50. package/dist/search-index.test.json +0 -1773
  51. package/dist/velin-agent.test.json +0 -684
package/README.de.md CHANGED
@@ -10,7 +10,7 @@
10
10
  ```
11
11
 
12
12
  [![Lizenz: MIT](https://img.shields.io/badge/Lizenz-MIT-2563eb?style=flat-square)](LICENSE)
13
- [![Release](https://img.shields.io/badge/Release-v0.9.0-2563eb?style=flat-square)](https://github.com/SkyliteDesign/velinstyle/releases/tag/v0.9.0)
13
+ [![Release](https://img.shields.io/badge/Release-v1.0.0-2563eb?style=flat-square)](https://github.com/SkyliteDesign/velinstyle/releases/tag/v1.0.0)
14
14
  [![npm version](https://img.shields.io/npm/v/@birdapi/velinstyle?style=flat-square)](https://www.npmjs.com/package/@birdapi/velinstyle)
15
15
  [![WCAG 2.2 AAA](https://img.shields.io/badge/WCAG_2.2-AAA-16a34a?style=flat-square)](https://velinstyle.info/docs/a11y.html)
16
16
  [![GitHub Stars](https://img.shields.io/github/stars/SkyliteDesign/velinstyle?style=flat-square)](https://github.com/SkyliteDesign/velinstyle/stargazers)
@@ -28,7 +28,7 @@ npm i @birdapi/velinstyle
28
28
 
29
29
  ---
30
30
 
31
- **VelinStyle** ist das **WCAG-2.2-AAA-CSS-Framework** mit nativer JavaScript-Runtime und Web Components — CSS-Utilities, 0.9.0-Module (Search, Motion, Highlight, Attributes, Meta) und Security-Toolchain, **ohne externe UI-Framework-Abhängigkeiten** im Kern.
31
+ **VelinStyle** ist das **WCAG-2.2-AAA-CSS-Framework** mit nativer JavaScript-Runtime und Web Components — CSS-Utilities, 1.0.0-Module (Search, Motion, Highlight, Attributes, Meta) und Security-Toolchain, **ohne externe UI-Framework-Abhängigkeiten** im Kern.
32
32
 
33
33
  Gedacht für Teams mit **lesbarem HTML**, **AAA-Token-Defaults** (AA über `data-velin-contrast="aa"`) und **CLI-Automatisierung** statt Utility-Chaos.
34
34
 
@@ -42,12 +42,14 @@ Gedacht für Teams mit **lesbarem HTML**, **AAA-Token-Defaults** (AA über `data
42
42
  - **35+ Komponenten** — semantische BEM-Klassen (`velin-btn`, `velin-card`, `velin-grid`, …)
43
43
  - **Motion-Runtime** — Reveal, Stagger, scroll-getriebene Animation, rAF-Scheduler
44
44
  - **VelinSearch** — fuzzy Offline-Suche mit Highlighting und Kategorien
45
- - **Syntax-Highlighting** — lazy In-View für JS, HTML, CSS, JSON und mehr
45
+ - **Syntax-Highlighting** — lazy In-View für JS, TS, HTML, CSS, JSON, Bash, Python, YAML, Go, Rust und mehr
46
46
  - **HTML-Attribute** — 27 deklarative Bridges (`velin-modal`, `velin-reveal`, `velin-scroll-top`, `velin-code`, …)
47
- - **Qualität** — 36/36 A11y-Component-Contracts, Playwright Cross-Browser-Smoke (`npm run test:e2e`), CLS-Platzhalter (`wc-placeholder.css`)
47
+ - **Qualität** — 38/38 A11y-Component-Contracts, Playwright Cross-Browser-Smoke (`npm run test:e2e`), CLS-Platzhalter (`wc-placeholder.css`)
48
48
  - **Security-Tools** — `scan`, PII-Regeln, Sanitize-API, gehärtete Komponenten
49
49
  - **CLI** — init, build, scan, scaffold, tokens, docs generate, perf audit, layout audit
50
50
  - **Velin-Meta** — `velin-agent.json`, `llms.txt` und Page-Level-Agent-JSON für KI-Assistenten
51
+ - **React-Adapter** — `@velinstyle/react` umhüllt jede kanonische Web Component mit typisierten Props und Events
52
+ - **Release-Guard** — `npm run release:check` bricht den Build bei Versions- oder Docs-Drift zwischen Framework und Site ab
51
53
 
52
54
  ---
53
55
 
@@ -70,8 +72,8 @@ npm i @birdapi/velinstyle
70
72
  **CDN (Version pinnen):**
71
73
 
72
74
  ```html
73
- <link rel="stylesheet" href="https://unpkg.com/@birdapi/velinstyle@0.9.0/dist/velinstyle.min.css">
74
- <script type="module" src="https://unpkg.com/@birdapi/velinstyle@0.9.0/dist/velinstyle-components.min.js"></script>
75
+ <link rel="stylesheet" href="https://unpkg.com/@birdapi/velinstyle@1.1.0/dist/velinstyle.min.css">
76
+ <script type="module" src="https://unpkg.com/@birdapi/velinstyle@1.1.0/dist/velinstyle-components.min.js"></script>
75
77
  ```
76
78
 
77
79
  Nach dem Klonen: `npm install && npm run build` — `dist/` wird erzeugt, nicht committet.
@@ -86,8 +88,8 @@ Nach dem Klonen: `npm install && npm run build` — `dist/` wird erzeugt, nicht
86
88
  <head>
87
89
  <meta charset="UTF-8">
88
90
  <meta name="viewport" content="width=device-width, initial-scale=1">
89
- <link rel="stylesheet" href="https://unpkg.com/@birdapi/velinstyle@0.9.0/dist/velinstyle.min.css">
90
- <script type="module" src="https://unpkg.com/@birdapi/velinstyle@0.9.0/dist/velinstyle-components.min.js"></script>
91
+ <link rel="stylesheet" href="https://unpkg.com/@birdapi/velinstyle@1.1.0/dist/velinstyle.min.css">
92
+ <script type="module" src="https://unpkg.com/@birdapi/velinstyle@1.1.0/dist/velinstyle-components.min.js"></script>
91
93
  </head>
92
94
  <body class="velin-p-6">
93
95
  <button type="button" class="velin-btn velin-btn--primary" velin-reveal="slide-up">Loslegen</button>
@@ -98,7 +100,7 @@ Nach dem Klonen: `npm install && npm run build` — `dist/` wird erzeugt, nicht
98
100
 
99
101
  ---
100
102
 
101
- ## Core-Module (0.9.0)
103
+ ## Core-Module (1.0.0)
102
104
 
103
105
  | Export | Beschreibung |
104
106
  | --- | --- |
@@ -112,9 +114,11 @@ Nach dem Klonen: `npm install && npm run build` — `dist/` wird erzeugt, nicht
112
114
 
113
115
  ## Web Components
114
116
 
115
- **36 kanonische** Custom Elements (38 Lazy-Loader-Einträge inkl. Legacy `velin-tooltip-wc` und `velin-stepper-wc`) — reines CSS, wenn kein Verhalten nötig ist. `src/base/wc-placeholder.css` verringert Layout-Shift vor dem Upgrade.
117
+ **38 kanonische** Custom Elements (40 Lazy-Loader-Einträge inkl. Legacy `velin-tooltip-wc` und `velin-stepper-wc`) — reines CSS, wenn kein Verhalten nötig ist. `src/base/wc-placeholder.css` verringert Layout-Shift vor dem Upgrade.
116
118
 
117
- Beispiele: `velin-modal`, `velin-search`, `velin-code-block`, `velin-drawer`, `velin-stepper`, `velin-tooltip`, `velin-toast`, `velin-persist`.
119
+ Beispiele: `velin-modal`, `velin-search`, `velin-code-block`, `velin-drawer`, `velin-stepper`, `velin-tooltip`, `velin-toast`, `velin-persist`, `velin-data-table`, `velin-form-summary`.
120
+
121
+ React-Wrapper für alle kanonischen Elemente liefert [`@velinstyle/react`](./packages/react/README.md).
118
122
 
119
123
  - [Komponenten-Dokumentation](https://velinstyle.info/docs/components/buttons.html)
120
124
  - [Generierte Web-Component-API](https://velinstyle.info/docs/generated/components/README.md)
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  ```
11
11
 
12
12
  [![License: MIT](https://img.shields.io/badge/License-MIT-2563eb?style=flat-square)](LICENSE)
13
- [![Release](https://img.shields.io/badge/Release-v0.9.0-2563eb?style=flat-square)](https://github.com/SkyliteDesign/velinstyle/releases/tag/v0.9.0)
13
+ [![Release](https://img.shields.io/badge/Release-v1.0.0-2563eb?style=flat-square)](https://github.com/SkyliteDesign/velinstyle/releases/tag/v1.0.0)
14
14
  [![npm version](https://img.shields.io/npm/v/@birdapi/velinstyle?style=flat-square)](https://www.npmjs.com/package/@birdapi/velinstyle)
15
15
  [![WCAG 2.2 AAA](https://img.shields.io/badge/WCAG_2.2-AAA-16a34a?style=flat-square)](https://velinstyle.info/docs/a11y.html)
16
16
  [![GitHub stars](https://img.shields.io/github/stars/SkyliteDesign/velinstyle?style=flat-square)](https://github.com/SkyliteDesign/velinstyle/stargazers)
@@ -28,7 +28,7 @@ npm i @birdapi/velinstyle
28
28
 
29
29
  ---
30
30
 
31
- **VelinStyle** is the **WCAG 2.2 AAA CSS framework** with native JavaScript runtime and Web Components — CSS utilities, 0.9.0 modules (search, motion, highlight, attributes, meta), and security tooling, with **no external UI framework dependencies** in the core.
31
+ **VelinStyle** is the **WCAG 2.2 AAA CSS framework** with native JavaScript runtime and Web Components — CSS utilities, 1.0.0 modules (search, motion, highlight, attributes, meta), and security tooling, with **no external UI framework dependencies** in the core.
32
32
 
33
33
  Built for teams who want **readable HTML**, **AAA token defaults** (AA via `data-velin-contrast="aa"`), and **CLI automation** instead of utility sprawl.
34
34
 
@@ -42,12 +42,14 @@ Built for teams who want **readable HTML**, **AAA token defaults** (AA via `data
42
42
  - **35+ components** — semantic BEM classes (`velin-btn`, `velin-card`, `velin-grid`, …)
43
43
  - **Motion runtime** — reveal, stagger, scroll-driven animation, rAF scheduler
44
44
  - **VelinSearch** — fuzzy offline search with highlighting and categories
45
- - **Syntax highlighting** — lazy in-view highlighting for JS, HTML, CSS, JSON, and more
45
+ - **Syntax highlighting** — lazy in-view highlighting for JS, TS, HTML, CSS, JSON, Bash, Python, YAML, Go, Rust, and more
46
46
  - **HTML attributes** — 27 declarative bridges (`velin-modal`, `velin-reveal`, `velin-scroll-top`, `velin-code`, …)
47
- - **Quality** — 36/36 component a11y contracts, Playwright cross-browser smoke (`npm run test:e2e`), CLS placeholders (`wc-placeholder.css`)
47
+ - **Quality** — 38/38 component a11y contracts, Playwright cross-browser smoke (`npm run test:e2e`), CLS placeholders (`wc-placeholder.css`)
48
48
  - **Security tools** — `scan`, PII rules, sanitize API, hardened components
49
49
  - **CLI** — init, build, scan, scaffold, tokens, docs generate, perf audit, layout audit
50
50
  - **Velin-Meta** — `velin-agent.json`, `llms.txt`, and page-level agent JSON for AI assistants
51
+ - **React adapter** — `@velinstyle/react` wraps every canonical Web Component with typed props and events
52
+ - **Release guard** — `npm run release:check` fails the build on version or docs drift between framework and site
51
53
 
52
54
  ---
53
55
 
@@ -70,8 +72,8 @@ npm i @birdapi/velinstyle
70
72
  **CDN (pin version):**
71
73
 
72
74
  ```html
73
- <link rel="stylesheet" href="https://unpkg.com/@birdapi/velinstyle@0.9.0/dist/velinstyle.min.css">
74
- <script type="module" src="https://unpkg.com/@birdapi/velinstyle@0.9.0/dist/velinstyle-components.min.js"></script>
75
+ <link rel="stylesheet" href="https://unpkg.com/@birdapi/velinstyle@1.1.0/dist/velinstyle.min.css">
76
+ <script type="module" src="https://unpkg.com/@birdapi/velinstyle@1.1.0/dist/velinstyle-components.min.js"></script>
75
77
  ```
76
78
 
77
79
  After cloning, run `npm install && npm run build` — `dist/` is generated, not committed.
@@ -86,8 +88,8 @@ After cloning, run `npm install && npm run build` — `dist/` is generated, not
86
88
  <head>
87
89
  <meta charset="UTF-8">
88
90
  <meta name="viewport" content="width=device-width, initial-scale=1">
89
- <link rel="stylesheet" href="https://unpkg.com/@birdapi/velinstyle@0.9.0/dist/velinstyle.min.css">
90
- <script type="module" src="https://unpkg.com/@birdapi/velinstyle@0.9.0/dist/velinstyle-components.min.js"></script>
91
+ <link rel="stylesheet" href="https://unpkg.com/@birdapi/velinstyle@1.1.0/dist/velinstyle.min.css">
92
+ <script type="module" src="https://unpkg.com/@birdapi/velinstyle@1.1.0/dist/velinstyle-components.min.js"></script>
91
93
  </head>
92
94
  <body class="velin-p-6">
93
95
  <button type="button" class="velin-btn velin-btn--primary" velin-reveal="slide-up">Ship it</button>
@@ -98,7 +100,7 @@ After cloning, run `npm install && npm run build` — `dist/` is generated, not
98
100
 
99
101
  ---
100
102
 
101
- ## Core modules (0.9.0)
103
+ ## Core modules (1.0.0)
102
104
 
103
105
  | Export | Description |
104
106
  | --- | --- |
@@ -112,9 +114,11 @@ After cloning, run `npm install && npm run build` — `dist/` is generated, not
112
114
 
113
115
  ## Web Components
114
116
 
115
- **36 canonical** custom elements (38 lazy-loader entries including legacy `velin-tooltip-wc` and `velin-stepper-wc`) — use plain CSS when you do not need behavior. `src/base/wc-placeholder.css` reduces layout shift before elements upgrade.
117
+ **38 canonical** custom elements (40 lazy-loader entries including legacy `velin-tooltip-wc` and `velin-stepper-wc`) — use plain CSS when you do not need behavior. `src/base/wc-placeholder.css` reduces layout shift before elements upgrade.
116
118
 
117
- Examples: `velin-modal`, `velin-search`, `velin-code-block`, `velin-drawer`, `velin-stepper`, `velin-tooltip`, `velin-toast`, `velin-persist`.
119
+ Examples: `velin-modal`, `velin-search`, `velin-code-block`, `velin-drawer`, `velin-stepper`, `velin-tooltip`, `velin-toast`, `velin-persist`, `velin-data-table`, `velin-form-summary`.
120
+
121
+ React wrappers for every canonical element ship in [`@velinstyle/react`](./packages/react/README.md).
118
122
 
119
123
  - [Component documentation](https://velinstyle.info/docs/components/buttons.html)
120
124
  - [Generated Web Component API](https://velinstyle.info/docs/generated/components/README.md)
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.9.0",
2
+ "version": "1.1.0",
3
3
  "commands": [
4
4
  {
5
5
  "name": "init",
@@ -21,10 +21,12 @@ export const WCAG_AAA_CRITERIA = [
21
21
  { id: '2.5.7', level: 'AA', name: 'Dragging Movements', owner: 'CSS + Sample', status: 'pass', testId: 'wcag22-dragging' },
22
22
  { id: '2.5.8', level: 'AA', name: 'Target Size', owner: 'CSS + WC', status: 'pass', testId: 'target-size' },
23
23
  { id: '3.2.6', level: 'A', name: 'Consistent Help', owner: 'CSS', status: 'pass', testId: 'consistent-help' },
24
- { id: '3.3.2', level: 'A', name: 'Labels or Instructions', owner: 'Forms + WC', status: 'partial', testId: 'scan-a11y' },
24
+ { id: '3.3.1', level: 'A', name: 'Error Identification', owner: 'velin-form-summary', status: 'pass', testId: 'velin-form-summary' },
25
+ { id: '3.3.2', level: 'A', name: 'Labels or Instructions', owner: 'Forms + WC + velin-form-summary', status: 'partial', testId: 'scan-a11y' },
26
+ { id: '3.3.3', level: 'AA', name: 'Error Suggestion', owner: 'velin-form-summary', status: 'pass', testId: 'velin-form-summary' },
25
27
  { id: '3.3.7', level: 'A', name: 'Redundant Entry', owner: 'velin-persist + Author', status: 'partial', testId: 'velin-persist' },
26
28
  { id: '3.3.8', level: 'AA', name: 'Accessible Authentication', owner: 'CSS + Sample', status: 'pass', testId: 'wcag22-auth' },
27
- { id: '4.1.2', level: 'A', name: 'Name, Role, Value', owner: 'Web Components', status: 'partial', testId: 'contracts' },
29
+ { id: '4.1.2', level: 'A', name: 'Name, Role, Value', owner: 'Web Components + velin-form-summary', status: 'partial', testId: 'contracts' },
28
30
  { id: '4.1.3', level: 'AA', name: 'Status Messages', owner: 'Announcer + WC', status: 'pass', testId: 'live-regions' },
29
31
  ];
30
32
 
package/cli/index.js CHANGED
@@ -11,6 +11,8 @@ const PKG_ROOT = join(__dirname, '..');
11
11
  const args = process.argv.slice(2);
12
12
  const command = args[0];
13
13
 
14
+ const CLI_VERSION = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf-8')).version;
15
+
14
16
  const LAYERS = ['tokens', 'reset', 'base', 'a11y', 'layout', 'components', 'utilities', 'security', 'helpers'];
15
17
 
16
18
  const LAYER_FILES = {
@@ -36,7 +38,7 @@ const LAYER_FILES = {
36
38
  components: [
37
39
  'components/button.css', 'components/card.css', 'components/input.css',
38
40
  'components/nav.css', 'components/alert.css', 'components/badge.css',
39
- 'components/table.css', 'components/tooltip.css', 'components/modal.css',
41
+ 'components/table.css', 'components/data-table.css', 'components/tooltip.css', 'components/modal.css',
40
42
  'components/breadcrumb.css', 'components/pagination.css', 'components/progress.css',
41
43
  'components/spinner.css', 'components/list-group.css', 'components/avatar.css',
42
44
  'components/switch.css', 'components/divider.css', 'components/chip.css',
@@ -83,7 +85,7 @@ function hasFlag(flag, alias) {
83
85
 
84
86
  function help() {
85
87
  console.log(`
86
- ${C.bold('VelinStyle CLI')} v0.9.0
88
+ ${C.bold('VelinStyle CLI')} v${CLI_VERSION}
87
89
 
88
90
  ${C.bold('Usage:')}
89
91
  velinstyle init Create velinstyle.config.js
@@ -33,6 +33,8 @@ export { default as VelinSparkline } from './velin-sparkline.js';
33
33
  export { default as VelinCounter } from './velin-counter.js';
34
34
  export { default as VelinLiveDot } from './velin-live-dot.js';
35
35
  export { default as VelinCodeBlock } from './velin-code-block.js';
36
+ export { default as VelinDataTable } from './velin-data-table.js';
37
+ export { default as VelinFormSummary } from './velin-form-summary.js';
36
38
  export { default as VelinSearch } from './velin-search.js';
37
39
  export { bindDeclarativeSearch } from './velin-search.js';
38
40
  export { initReveal, initMotion, velinMotion } from './velin-reveal.js';
@@ -13,10 +13,12 @@ export const COMPONENT_LOADERS = {
13
13
  'velin-copy': () => import('../velin-copy.js'),
14
14
  'velin-countdown': () => import('../velin-countdown.js'),
15
15
  'velin-counter': () => import('../velin-counter.js'),
16
+ 'velin-data-table': () => import('../velin-data-table.js'),
16
17
  'velin-dialog': () => import('../velin-dialog.js'),
17
18
  'velin-drawer': () => import('../velin-drawer.js'),
18
19
  'velin-dropdown': () => import('../velin-dropdown.js'),
19
20
  'velin-email': () => import('../velin-email.js'),
21
+ 'velin-form-summary': () => import('../velin-form-summary.js'),
20
22
  'velin-icon': () => import('../velin-icon.js'),
21
23
  'velin-lightbox': () => import('../velin-lightbox.js'),
22
24
  'velin-live-dot': () => import('../velin-live-dot.js'),
@@ -0,0 +1,361 @@
1
+ import { announce } from './a11y-utils.js';
2
+
3
+ /**
4
+ * Progressive enhancement for a light-DOM `<table>`: sorting, filtering and
5
+ * pagination without replacing the semantic markup.
6
+ *
7
+ * The table keeps working with JavaScript disabled — every feature is layered
8
+ * on top of the rows that are already in the document.
9
+ */
10
+
11
+ const SORT_TYPES = new Set(['text', 'number', 'date']);
12
+
13
+ let warnedMissingName = false;
14
+
15
+ /** @param {HTMLElement} cell */
16
+ function sortValue(cell, type) {
17
+ const raw = cell?.dataset?.sortValue ?? cell?.textContent ?? '';
18
+ const text = raw.trim();
19
+ if (type === 'number') {
20
+ const num = Number.parseFloat(text.replace(/[^\d.,-]/g, '').replace(',', '.'));
21
+ return Number.isNaN(num) ? Number.NEGATIVE_INFINITY : num;
22
+ }
23
+ if (type === 'date') {
24
+ const time = Date.parse(text);
25
+ return Number.isNaN(time) ? Number.NEGATIVE_INFINITY : time;
26
+ }
27
+ return text.toLowerCase();
28
+ }
29
+
30
+ class VelinDataTable extends HTMLElement {
31
+ static get observedAttributes() {
32
+ return ['page-size', 'filter-input', 'empty-text', 'label'];
33
+ }
34
+
35
+ constructor() {
36
+ super();
37
+ this._table = null;
38
+ this._page = 1;
39
+ this._query = '';
40
+ this._sortIndex = -1;
41
+ this._sortDirection = 'ascending';
42
+ this._filterEl = null;
43
+ this._pagination = null;
44
+ this._emptyRow = null;
45
+ this._filterTimer = null;
46
+ this._onFilterInput = this._onFilterInput.bind(this);
47
+ }
48
+
49
+ connectedCallback() {
50
+ this.classList.add('velin-data-table');
51
+ // Rows may still be streaming in when the element upgrades.
52
+ requestAnimationFrame(() => this._init());
53
+ }
54
+
55
+ disconnectedCallback() {
56
+ if (this._filterEl) this._filterEl.removeEventListener('input', this._onFilterInput);
57
+ if (this._filterTimer) clearTimeout(this._filterTimer);
58
+ }
59
+
60
+ attributeChangedCallback(name, previous, next) {
61
+ if (previous === next || !this._table) return;
62
+ if (name === 'page-size') {
63
+ this._page = 1;
64
+ this._render();
65
+ } else if (name === 'filter-input') {
66
+ this._bindFilterInput();
67
+ } else if (name === 'label') {
68
+ this._ensureAccessibleName();
69
+ }
70
+ }
71
+
72
+ // ── Public API ─────────────────────────────────────────────────────────────
73
+
74
+ /** @returns {HTMLTableRowElement[]} */
75
+ get rows() {
76
+ const body = this._table?.tBodies?.[0];
77
+ return body ? [...body.rows].filter((row) => row !== this._emptyRow) : [];
78
+ }
79
+
80
+ /** Rows matching the current filter, across all pages. */
81
+ get matchingRows() {
82
+ return this.rows.filter((row) => !row.dataset.velinFiltered);
83
+ }
84
+
85
+ /** Rows visible on the current page. */
86
+ get visibleRows() {
87
+ return this.matchingRows.filter((row) => !row.hidden);
88
+ }
89
+
90
+ get page() {
91
+ return this._page;
92
+ }
93
+
94
+ get pageSize() {
95
+ const size = Number.parseInt(this.getAttribute('page-size') || '', 10);
96
+ return Number.isFinite(size) && size > 0 ? size : 0;
97
+ }
98
+
99
+ get pageCount() {
100
+ const size = this.pageSize;
101
+ if (!size) return 1;
102
+ return Math.max(1, Math.ceil(this.matchingRows.length / size));
103
+ }
104
+
105
+ /**
106
+ * @param {number} index Column index
107
+ * @param {'ascending' | 'descending'} [direction]
108
+ */
109
+ sort(index, direction) {
110
+ const headers = this._headers();
111
+ const header = headers[index];
112
+ if (!header) return;
113
+ const type = this._sortType(header);
114
+ if (type === 'none') return;
115
+
116
+ this._sortDirection = direction
117
+ || (this._sortIndex === index && this._sortDirection === 'ascending' ? 'descending' : 'ascending');
118
+ this._sortIndex = index;
119
+
120
+ const factor = this._sortDirection === 'ascending' ? 1 : -1;
121
+ const body = this._table.tBodies[0];
122
+ const sorted = this.rows.slice().sort((a, b) => {
123
+ const av = sortValue(a.cells[index], type);
124
+ const bv = sortValue(b.cells[index], type);
125
+ if (av < bv) return -1 * factor;
126
+ if (av > bv) return 1 * factor;
127
+ return 0;
128
+ });
129
+ for (const row of sorted) body.appendChild(row);
130
+
131
+ this._syncSortState();
132
+ this._page = 1;
133
+ this._render();
134
+
135
+ const label = header.dataset.sortLabel || header.textContent.trim();
136
+ announce(`${label} sorted ${this._sortDirection}`);
137
+ this.dispatchEvent(new CustomEvent('velin-data-table-sort', {
138
+ bubbles: true,
139
+ detail: { index, direction: this._sortDirection, column: label },
140
+ }));
141
+ }
142
+
143
+ /** @param {string} query */
144
+ filter(query) {
145
+ this._query = String(query || '').trim().toLowerCase();
146
+ for (const row of this.rows) {
147
+ const match = !this._query || this._rowText(row).includes(this._query);
148
+ if (match) delete row.dataset.velinFiltered;
149
+ else row.dataset.velinFiltered = 'true';
150
+ }
151
+ this._page = 1;
152
+ this._render();
153
+
154
+ const count = this.matchingRows.length;
155
+ announce(count === 1 ? '1 row matches' : `${count} rows match`);
156
+ this.dispatchEvent(new CustomEvent('velin-data-table-filter', {
157
+ bubbles: true,
158
+ detail: { query: this._query, count },
159
+ }));
160
+ }
161
+
162
+ /** @param {number} page */
163
+ goToPage(page) {
164
+ const target = Math.min(Math.max(1, Math.trunc(page) || 1), this.pageCount);
165
+ if (target === this._page) return;
166
+ this._page = target;
167
+ this._render();
168
+ announce(`Page ${this._page} of ${this.pageCount}`);
169
+ this.dispatchEvent(new CustomEvent('velin-data-table-page', {
170
+ bubbles: true,
171
+ detail: { page: this._page, pageCount: this.pageCount },
172
+ }));
173
+ }
174
+
175
+ // ── Setup ──────────────────────────────────────────────────────────────────
176
+
177
+ _init() {
178
+ this._table = this.querySelector('table');
179
+ if (!this._table || !this._table.tBodies.length) return;
180
+
181
+ this._ensureAccessibleName();
182
+ this._setupSorting();
183
+ this._bindFilterInput();
184
+ this._render();
185
+ }
186
+
187
+ _headers() {
188
+ const headRow = this._table.tHead?.rows?.[0];
189
+ return headRow ? [...headRow.cells] : [];
190
+ }
191
+
192
+ /** @param {HTMLTableCellElement} header */
193
+ _sortType(header) {
194
+ const declared = (header.dataset.sort || '').toLowerCase();
195
+ if (declared === 'none') return 'none';
196
+ if (SORT_TYPES.has(declared)) return declared;
197
+ return this.hasAttribute('sortable') ? 'text' : 'none';
198
+ }
199
+
200
+ /**
201
+ * A table without a name is unusable with a screen reader, so surface it
202
+ * instead of failing silently (WCAG 1.3.1 / 2.4.6).
203
+ */
204
+ _ensureAccessibleName() {
205
+ const table = this._table;
206
+ if (!table) return;
207
+ const label = this.getAttribute('label');
208
+ const hasName = table.caption?.textContent.trim()
209
+ || table.getAttribute('aria-label')?.trim()
210
+ || table.getAttribute('aria-labelledby')?.trim();
211
+
212
+ if (!hasName && label) {
213
+ table.setAttribute('aria-label', label);
214
+ return;
215
+ }
216
+ if (!hasName && !warnedMissingName) {
217
+ warnedMissingName = true;
218
+ console.warn('[velinstyle] <velin-data-table> needs a <caption>, aria-label, or a label attribute.');
219
+ }
220
+ }
221
+
222
+ /** Sortable headers get a real button so keyboard and AT support come for free. */
223
+ _setupSorting() {
224
+ this._headers().forEach((header, index) => {
225
+ if (this._sortType(header) === 'none') return;
226
+ if (header.querySelector('.velin-data-table__sort')) return;
227
+
228
+ const label = header.textContent.trim();
229
+ header.dataset.sortLabel = label;
230
+ header.setAttribute('aria-sort', 'none');
231
+ header.classList.add('velin-data-table__th');
232
+
233
+ const button = document.createElement('button');
234
+ button.type = 'button';
235
+ button.className = 'velin-data-table__sort';
236
+ button.textContent = label;
237
+
238
+ const icon = document.createElement('span');
239
+ icon.className = 'velin-data-table__sort-icon';
240
+ icon.setAttribute('aria-hidden', 'true');
241
+ button.appendChild(icon);
242
+
243
+ button.addEventListener('click', () => this.sort(index));
244
+ header.textContent = '';
245
+ header.appendChild(button);
246
+ });
247
+ }
248
+
249
+ _syncSortState() {
250
+ this._headers().forEach((header, index) => {
251
+ if (this._sortType(header) === 'none') return;
252
+ const active = index === this._sortIndex;
253
+ header.setAttribute('aria-sort', active ? this._sortDirection : 'none');
254
+ header.querySelector('.velin-data-table__sort')?.classList.toggle('velin-data-table__sort--active', active);
255
+ });
256
+ }
257
+
258
+ _bindFilterInput() {
259
+ if (this._filterEl) this._filterEl.removeEventListener('input', this._onFilterInput);
260
+ const selector = this.getAttribute('filter-input');
261
+ this._filterEl = selector ? document.querySelector(selector) : null;
262
+ if (this._filterEl) this._filterEl.addEventListener('input', this._onFilterInput);
263
+ }
264
+
265
+ _onFilterInput(event) {
266
+ if (this._filterTimer) clearTimeout(this._filterTimer);
267
+ const value = event.target.value;
268
+ this._filterTimer = setTimeout(() => this.filter(value), 150);
269
+ }
270
+
271
+ /** @param {HTMLTableRowElement} row */
272
+ _rowText(row) {
273
+ const scoped = [...row.cells].filter((cell) => cell.hasAttribute('data-filter'));
274
+ const cells = scoped.length ? scoped : [...row.cells];
275
+ return cells.map((cell) => cell.textContent || '').join(' ').toLowerCase();
276
+ }
277
+
278
+ // ── Rendering ──────────────────────────────────────────────────────────────
279
+
280
+ _render() {
281
+ const size = this.pageSize;
282
+ this._page = Math.min(this._page, this.pageCount);
283
+ const matching = this.matchingRows;
284
+ const start = size ? (this._page - 1) * size : 0;
285
+ const end = size ? start + size : matching.length;
286
+
287
+ for (const row of this.rows) {
288
+ const index = matching.indexOf(row);
289
+ row.hidden = index === -1 || index < start || index >= end;
290
+ }
291
+
292
+ this._renderEmptyState(matching.length === 0);
293
+ this._renderPagination();
294
+ }
295
+
296
+ /** @param {boolean} isEmpty */
297
+ _renderEmptyState(isEmpty) {
298
+ if (!isEmpty) {
299
+ this._emptyRow?.remove();
300
+ this._emptyRow = null;
301
+ return;
302
+ }
303
+ if (this._emptyRow?.isConnected) return;
304
+
305
+ const columns = this._headers().length || 1;
306
+ const row = document.createElement('tr');
307
+ row.className = 'velin-data-table__empty';
308
+ const cell = document.createElement('td');
309
+ cell.colSpan = columns;
310
+ cell.textContent = this.getAttribute('empty-text') || 'No matching rows';
311
+ row.appendChild(cell);
312
+ this._table.tBodies[0].appendChild(row);
313
+ this._emptyRow = row;
314
+ }
315
+
316
+ _renderPagination() {
317
+ if (!this.pageSize || this.pageCount <= 1) {
318
+ this._pagination?.remove();
319
+ this._pagination = null;
320
+ return;
321
+ }
322
+
323
+ if (!this._pagination?.isConnected) {
324
+ const nav = document.createElement('nav');
325
+ nav.className = 'velin-data-table__pagination';
326
+ nav.setAttribute('aria-label', this.getAttribute('pagination-label') || 'Table pagination');
327
+
328
+ const previous = document.createElement('button');
329
+ previous.type = 'button';
330
+ previous.className = 'velin-btn velin-btn--outline velin-btn--sm';
331
+ previous.dataset.velinPage = 'previous';
332
+ previous.textContent = this.getAttribute('previous-text') || 'Previous';
333
+ previous.addEventListener('click', () => this.goToPage(this._page - 1));
334
+
335
+ const status = document.createElement('p');
336
+ status.className = 'velin-data-table__page-status';
337
+ status.dataset.velinPage = 'status';
338
+
339
+ const next = document.createElement('button');
340
+ next.type = 'button';
341
+ next.className = 'velin-btn velin-btn--outline velin-btn--sm';
342
+ next.dataset.velinPage = 'next';
343
+ next.textContent = this.getAttribute('next-text') || 'Next';
344
+ next.addEventListener('click', () => this.goToPage(this._page + 1));
345
+
346
+ nav.append(previous, status, next);
347
+ this.appendChild(nav);
348
+ this._pagination = nav;
349
+ }
350
+
351
+ const status = this._pagination.querySelector('[data-velin-page="status"]');
352
+ if (status) status.textContent = `Page ${this._page} of ${this.pageCount}`;
353
+ const previous = this._pagination.querySelector('[data-velin-page="previous"]');
354
+ const next = this._pagination.querySelector('[data-velin-page="next"]');
355
+ if (previous) previous.disabled = this._page <= 1;
356
+ if (next) next.disabled = this._page >= this.pageCount;
357
+ }
358
+ }
359
+
360
+ customElements.define('velin-data-table', VelinDataTable);
361
+ export default VelinDataTable;