@gjsify/adwaita-web 0.14.0 → 0.16.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 (49) hide show
  1. package/README.md +58 -0
  2. package/dist/adwaita-web.css +617 -0
  3. package/dist/adwaita-web.css.map +1 -1
  4. package/lib/types/adw-data-grid.spec.d.ts +1 -0
  5. package/lib/types/adw-dialog.spec.d.ts +1 -0
  6. package/lib/types/adw-drop-down.spec.d.ts +1 -0
  7. package/lib/types/elements/adw-data-grid.d.ts +60 -0
  8. package/lib/types/elements/adw-dialog.d.ts +51 -0
  9. package/lib/types/elements/adw-drop-down.d.ts +63 -0
  10. package/lib/types/elements/adw-menu-button.d.ts +34 -0
  11. package/lib/types/elements/adw-view-stack.d.ts +37 -0
  12. package/lib/types/elements/adw-view-switcher-bar.d.ts +27 -0
  13. package/lib/types/index.d.ts +11 -0
  14. package/lib/types/source-view/adw-source-view.d.ts +75 -0
  15. package/lib/types/source-view/asm6502.d.ts +28 -0
  16. package/lib/types/source-view/hex.d.ts +27 -0
  17. package/lib/types/source-view/index.d.ts +4 -0
  18. package/lib/types/source-view/theme.d.ts +15 -0
  19. package/lib/types/styles.generated.d.ts +1 -1
  20. package/package.json +24 -12
  21. package/scss/_data_grid.scss +144 -0
  22. package/scss/_dialog.scss +220 -0
  23. package/scss/_drop_down.scss +185 -0
  24. package/scss/_menu_button.scss +151 -0
  25. package/scss/_root.scss +22 -0
  26. package/scss/_theme.scss +5 -0
  27. package/scss/_variables.scss +9 -0
  28. package/scss/_view_stack.scss +29 -0
  29. package/scss/_view_switcher_bar.scss +94 -0
  30. package/scss/adwaita-skin.scss +7 -0
  31. package/src/adw-data-grid.spec.ts +243 -0
  32. package/src/adw-dialog.spec.ts +169 -0
  33. package/src/adw-drop-down.spec.ts +186 -0
  34. package/src/elements/adw-data-grid.ts +335 -0
  35. package/src/elements/adw-dialog.ts +329 -0
  36. package/src/elements/adw-drop-down.ts +405 -0
  37. package/src/elements/adw-menu-button.ts +247 -0
  38. package/src/elements/adw-view-stack.ts +174 -0
  39. package/src/elements/adw-view-switcher-bar.ts +168 -0
  40. package/src/index.ts +16 -0
  41. package/src/source-view/adw-source-view.ts +436 -0
  42. package/src/source-view/asm6502.ts +232 -0
  43. package/src/source-view/hex.ts +43 -0
  44. package/src/source-view/index.ts +28 -0
  45. package/src/source-view/theme.ts +224 -0
  46. package/src/test.browser.mts +14 -0
  47. package/src/index.js +0 -1
  48. package/src/styles.generated.js +0 -2976
  49. package/src/styles2.generated.js +0 -2976
@@ -0,0 +1,151 @@
1
+ // _menu_button.scss — adw-menu-button styling: a flat (or circular) icon button
2
+ // that drops an Adwaita-styled popover menu (the web counterpart of
3
+ // Adw.MenuButton / Gtk.MenuButton). The button mirrors libadwaita's flat/circular
4
+ // `menubutton > button`; the popover reproduces `popover.menu > contents` with
5
+ // its soft triple-shadow surface and `modelbutton`-style item rows.
6
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_buttons.scss (menubutton flat/circular)
7
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_popovers.scss (popover > contents)
8
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_menus.scss (popover.menu modelbutton)
9
+ // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
10
+ // Modifications: Adapted for @gjsify/adwaita-web Web Components.
11
+
12
+ .adw-menu-button {
13
+ position: relative;
14
+ box-sizing: border-box;
15
+ display: inline-flex;
16
+ vertical-align: middle;
17
+
18
+ // The icon button — square with the button radius by default.
19
+ .adw-menu-button-button {
20
+ box-sizing: border-box;
21
+ display: inline-flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ min-width: 34px;
25
+ min-height: 34px;
26
+ margin: 0;
27
+ padding: 0 8px;
28
+ border: none;
29
+ border-radius: var(--button-radius);
30
+ background-color: var(--button-bg-color);
31
+ color: var(--window-fg-color);
32
+ cursor: pointer;
33
+ transition: background-color 0.1s ease;
34
+
35
+ .adw-menu-button-icon {
36
+ width: 16px;
37
+ height: 16px;
38
+ flex: none;
39
+ }
40
+
41
+ &:hover {
42
+ background-color: var(--button-hover-color);
43
+ }
44
+
45
+ &:active {
46
+ background-color: var(--button-active-color);
47
+ }
48
+
49
+ &:focus-visible {
50
+ outline: 2px solid var(--accent-color);
51
+ outline-offset: 1px;
52
+ }
53
+ }
54
+
55
+ // Flat — no resting fill (the header app-menu idiom).
56
+ &.flat .adw-menu-button-button {
57
+ background-color: transparent;
58
+ }
59
+
60
+ // The open state keeps a subtle fill so the button reads as "checked".
61
+ &.active .adw-menu-button-button {
62
+ background-color: var(--button-active-color);
63
+ }
64
+
65
+ // Circular — a round 34×34 button (window-control / FAB idiom).
66
+ &.circular .adw-menu-button-button {
67
+ padding: 0;
68
+ border-radius: 9999px;
69
+ }
70
+
71
+ &.disabled {
72
+ opacity: 0.45;
73
+
74
+ .adw-menu-button-button {
75
+ cursor: default;
76
+ pointer-events: none;
77
+ }
78
+ }
79
+
80
+ // --- Popover menu ---
81
+ // The elevated menu surface below the button, right-aligned like a header-end
82
+ // app menu. Reproduces libadwaita's popover.menu > contents.
83
+ .adw-menu-button-popover {
84
+ position: absolute;
85
+ top: calc(100% + 6px);
86
+ right: 0;
87
+ z-index: 10;
88
+ display: flex;
89
+ flex-direction: column;
90
+ min-width: 200px;
91
+ padding: var(--spacing-xs);
92
+ border-radius: var(--card-radius);
93
+ background-color: var(--popover-bg-color, var(--window-bg-color, #fff));
94
+ color: var(--popover-fg-color, var(--window-fg-color));
95
+ box-shadow:
96
+ 0 0 0 1px rgba(0, 0, 0, 0.05),
97
+ 0 1px 5px 1px rgba(0, 0, 0, 0.09),
98
+ 0 2px 14px 3px rgba(0, 0, 0, 0.05);
99
+
100
+ &[hidden] {
101
+ display: none;
102
+ }
103
+ }
104
+
105
+ // Optional section heading atop the menu (libadwaita label.title).
106
+ .adw-menu-button-title {
107
+ padding: 4px var(--spacing-s) 6px;
108
+ font-weight: 700;
109
+ opacity: var(--dim-opacity);
110
+ }
111
+
112
+ // A menu item row — a flat rounded row that fills on hover/active.
113
+ .adw-menu-button-item {
114
+ box-sizing: border-box;
115
+ display: flex;
116
+ align-items: center;
117
+ gap: var(--spacing-xs);
118
+ width: 100%;
119
+ min-height: 32px;
120
+ margin: 0;
121
+ padding: var(--spacing-xs) var(--spacing-s);
122
+ border: none;
123
+ border-radius: calc(var(--button-radius) - 2px);
124
+ background-color: transparent;
125
+ color: inherit;
126
+ font-family: var(--font-family);
127
+ font-size: var(--font-size-base);
128
+ text-align: start;
129
+ cursor: pointer;
130
+ transition: background-color 0.1s ease;
131
+
132
+ .adw-menu-button-item-icon {
133
+ width: 16px;
134
+ height: 16px;
135
+ flex: none;
136
+ }
137
+
138
+ &:hover {
139
+ background-color: var(--button-hover-color);
140
+ }
141
+
142
+ &:active {
143
+ background-color: var(--button-active-color);
144
+ }
145
+
146
+ &:focus-visible {
147
+ outline: 2px solid var(--accent-color);
148
+ outline-offset: -2px;
149
+ }
150
+ }
151
+ }
@@ -0,0 +1,22 @@
1
+ // _root.scss — opt-in page surface for chrome-less layouts.
2
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_window.scss (surface tokens)
3
+ // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
4
+ // Modifications: original opt-in helper for @gjsify/adwaita-web.
5
+ //
6
+ // The components theme via the `--window-*` CSS custom properties, but only
7
+ // `adw-window` actually PAINTS the window surface (background + text colour).
8
+ // A page laid out WITHOUT an `<adw-window>` (e.g. a plain browser view that
9
+ // hosts rows/cards directly) therefore renders on the browser default (white)
10
+ // and its native form controls / scrollbars don't follow the light/dark scheme.
11
+ //
12
+ // Applying `.adw-root` to `<body>` (or `<html>` / the `:root` element) binds the
13
+ // Adwaita window surface + `color-scheme: light dark` to the document, so a
14
+ // window-less layout is themed exactly like the components inside it. This is
15
+ // OPT-IN and additive — it does not change any component's own styling.
16
+ .adw-root {
17
+ background-color: var(--window-bg-color);
18
+ color: var(--window-fg-color);
19
+ color-scheme: light dark;
20
+ font-family: var(--font-family);
21
+ font-size: var(--font-size-base);
22
+ }
package/scss/_theme.scss CHANGED
@@ -18,6 +18,11 @@
18
18
  --card-fg-color: #ffffff;
19
19
  --card-shade-color: rgba(0, 0, 6, 0.36);
20
20
  --accent-color: #78aeed;
21
+ --popover-bg-color: #36363a;
22
+ --popover-fg-color: #ffffff;
23
+ --popover-shade-color: rgba(0, 0, 6, 0.36);
24
+ // A deeper scrim in dark mode so the dimming reads against a dark window.
25
+ --dialog-scrim-color: rgba(0, 0, 6, 0.55);
21
26
  --button-bg-color: rgba(255, 255, 255, 0.1);
22
27
  --button-hover-color: rgba(255, 255, 255, 0.15);
23
28
  --button-active-color: rgba(255, 255, 255, 0.22);
@@ -37,6 +37,15 @@
37
37
  --accent-fg-color: #ffffff;
38
38
  --accent-color: #1c71d8;
39
39
 
40
+ // Popover (menus, dropdowns) — an elevated surface above the window.
41
+ --popover-bg-color: #ffffff;
42
+ --popover-fg-color: rgba(0, 0, 6, 0.8);
43
+ --popover-shade-color: rgba(0, 0, 6, 0.07);
44
+
45
+ // Dialog scrim — the dimming wash behind a presented dialog (Adw dims with
46
+ // --shade-color at double alpha; this is the equivalent neutral wash).
47
+ --dialog-scrim-color: rgba(0, 0, 6, 0.32);
48
+
40
49
  // Buttons
41
50
  --button-bg-color: rgba(0, 0, 6, 0.08);
42
51
  --button-hover-color: rgba(0, 0, 6, 0.13);
@@ -0,0 +1,29 @@
1
+ // _view_stack.scss — adw-view-stack styling: a standalone named-page content
2
+ // container that shows exactly one page at a time (the web counterpart of
3
+ // Adw.ViewStack, decoupled from any switcher). It carries no chrome of its own —
4
+ // a paired adw-view-switcher-bar (or a header-bar switcher) drives selection.
5
+ // Reference: refs/libadwaita/src/adw-view-stack.c (AdwViewStack)
6
+ // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
7
+ // Modifications: Adapted for @gjsify/adwaita-web Web Components.
8
+
9
+ adw-view-stack {
10
+ display: flex;
11
+ flex-direction: column;
12
+ box-sizing: border-box;
13
+ // vexpand-equivalent — fill the space a switcher shell allots to content.
14
+ flex: 1 1 auto;
15
+ min-height: 0;
16
+
17
+ // Exactly one child is shown; the rest are laid out of flow. `hidden` is set
18
+ // in JS too, but the class keeps the visible page a flex child that grows.
19
+ .adw-view-stack-child {
20
+ display: none;
21
+
22
+ &.active-view {
23
+ display: flex;
24
+ flex-direction: column;
25
+ flex: 1 1 auto;
26
+ min-height: 0;
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,94 @@
1
+ // _view_switcher_bar.scss — adw-view-switcher-bar styling: the narrow BOTTOM
2
+ // bar of homogeneous icon-over-label buttons bound to an adw-view-stack (the web
3
+ // counterpart of Adw.ViewSwitcherBar). It sits at the window bottom with a top
4
+ // hairline over a headerbar-toned surface, mirroring libadwaita's `.narrow`
5
+ // view-switcher inside an actionbar. The active button's label is accent-tinted.
6
+ // Reference: refs/libadwaita/src/adw-view-switcher-bar.c (AdwViewSwitcherBar)
7
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_view-switcher.scss (.narrow policy)
8
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_toolbars.scss (actionbar top border)
9
+ // Copyright (c) GNOME contributors (libadwaita). LGPLv2.1+.
10
+ // Modifications: Adapted for @gjsify/adwaita-web Web Components.
11
+
12
+ adw-view-switcher-bar {
13
+ display: block;
14
+ box-sizing: border-box;
15
+ // The bottom bar over a headerbar-toned surface with a top hairline (the
16
+ // libadwaita `actionbar` inset top border), NOT a bottom border.
17
+ background-color: var(--headerbar-bg-color);
18
+ border-top: 1px solid var(--separator-color);
19
+ padding: 7px 0; // libadwaita `viewswitcherbar > box` vertical padding
20
+
21
+ // The homogeneous button row — every button gets equal width.
22
+ .adw-view-switcher-bar-box {
23
+ display: flex;
24
+ align-items: stretch;
25
+ // The `.narrow` view-switcher drops to a 6px min-height (icon-over-label).
26
+ min-height: 6px;
27
+ }
28
+
29
+ // Each button — icon stacked above a small label, a flat transparent toggle
30
+ // that fills on hover/active and marks the selected page with an accent
31
+ // label (the neutral fill stays the same across states, like libadwaita).
32
+ .adw-view-switcher-bar-button {
33
+ box-sizing: border-box;
34
+ display: inline-flex;
35
+ flex: 1 1 0; // homogeneous — equal width
36
+ flex-direction: column;
37
+ align-items: center;
38
+ justify-content: center;
39
+ gap: 1px;
40
+ margin: 0;
41
+ padding: 4px 3px 2px;
42
+ border: none;
43
+ border-radius: var(--button-radius);
44
+ background-color: transparent;
45
+ color: var(--window-fg-color);
46
+ font-family: var(--font-family);
47
+ font-size: 0.75rem;
48
+ font-weight: 700;
49
+ line-height: 1.2;
50
+ cursor: pointer;
51
+ transition:
52
+ color 0.2s ease,
53
+ background-color 0.2s ease;
54
+ user-select: none;
55
+ -webkit-user-select: none;
56
+
57
+ .adw-view-switcher-bar-icon {
58
+ width: 16px;
59
+ height: 16px;
60
+ flex: none;
61
+ }
62
+
63
+ .adw-view-switcher-bar-label {
64
+ min-height: 18px;
65
+ line-height: 18px;
66
+ white-space: nowrap;
67
+ overflow: hidden;
68
+ text-overflow: ellipsis;
69
+ }
70
+
71
+ &:hover {
72
+ background-color: var(--button-hover-color);
73
+ }
74
+
75
+ &:active {
76
+ background-color: var(--button-active-color);
77
+ }
78
+
79
+ &:focus-visible {
80
+ outline: 2px solid var(--accent-color);
81
+ outline-offset: -2px;
82
+ }
83
+
84
+ // The selected page — its label turns accent-coloured. (The icon stays
85
+ // theme-fg, matching the NS twin's documented fidelity note.)
86
+ &.active {
87
+ color: var(--accent-color);
88
+
89
+ .adw-view-switcher-bar-label {
90
+ color: var(--accent-color);
91
+ }
92
+ }
93
+ }
94
+ }
@@ -9,16 +9,20 @@
9
9
  @use 'icons.generated';
10
10
  @use 'variables';
11
11
  @use 'theme';
12
+ @use 'root';
12
13
  @use 'window';
13
14
  @use 'headerbar';
14
15
  @use 'window_title';
15
16
  @use 'button';
16
17
  @use 'button_content';
17
18
  @use 'split_button';
19
+ @use 'menu_button';
20
+ @use 'drop_down';
18
21
  @use 'toggle_group';
19
22
  @use 'entry';
20
23
  @use 'preferences';
21
24
  @use 'card';
25
+ @use 'data_grid';
22
26
  @use 'clamp';
23
27
  @use 'spinner';
24
28
  @use 'avatar';
@@ -42,12 +46,15 @@
42
46
  @use 'tab_view';
43
47
  @use 'view_switcher';
44
48
  @use 'inline_view_switcher';
49
+ @use 'view_stack';
50
+ @use 'view_switcher_bar';
45
51
  @use 'navigation_view';
46
52
  @use 'bottom_sheet';
47
53
  @use 'sidebar';
48
54
  @use 'about_dialog';
49
55
  @use 'alert_dialog';
50
56
  @use 'preferences_dialog';
57
+ @use 'dialog';
51
58
 
52
59
  // Ensure the `hidden` attribute always wins: components that toggle visibility
53
60
  // via `element.hidden = true` set the HTML attribute, but a custom `display`
@@ -0,0 +1,243 @@
1
+ // DOM-level behaviour tests for <adw-data-grid>. Runs in a real browser via the
2
+ // @gjsify/adwaita-web browser test axis. Asserts column parsing + alignment
3
+ // classes, the derived grid tracks, per-cell rendering of pre-formatted strings,
4
+ // the accounting row variants (normal / section / subtotal / total), and the
5
+ // row-activated interaction (element-level + per-row, keyboard + pointer).
6
+ import { describe, it, expect } from '@gjsify/unit';
7
+
8
+ import type { AdwDataGrid, AdwDataGridColumn, AdwDataGridRow } from './elements/adw-data-grid.js';
9
+
10
+ function makeGrid(columns?: AdwDataGridColumn[], rows?: AdwDataGridRow[]): AdwDataGrid {
11
+ const el = document.createElement('adw-data-grid') as AdwDataGrid;
12
+ if (columns) el.columns = columns;
13
+ if (rows) el.rows = rows;
14
+ document.body.appendChild(el);
15
+ return el;
16
+ }
17
+
18
+ const BWA_COLUMNS: AdwDataGridColumn[] = [
19
+ { key: 'position', label: 'BWA-Position' },
20
+ { key: 'total', label: 'Σ Jahr', numeric: true },
21
+ ];
22
+
23
+ export const AdwDataGridTest = async () => {
24
+ await describe('adw-data-grid columns', async () => {
25
+ await it('renders one header cell per column with the label', async () => {
26
+ const grid = makeGrid(BWA_COLUMNS, []);
27
+ const header = grid.querySelector('.adw-data-grid-row.header');
28
+ expect(header).toBeTruthy();
29
+ const cells = grid.querySelectorAll('.adw-data-grid-row.header .adw-data-grid-cell');
30
+ expect(cells.length).toBe(2);
31
+ expect(cells[0].textContent).toBe('BWA-Position');
32
+ expect(cells[1].textContent).toBe('Σ Jahr');
33
+ grid.remove();
34
+ });
35
+
36
+ await it('falls back to the key when no label is given', async () => {
37
+ const grid = makeGrid([{ key: 'amount' }], []);
38
+ const cell = grid.querySelector('.adw-data-grid-row.header .adw-data-grid-cell');
39
+ expect(cell?.textContent).toBe('amount');
40
+ grid.remove();
41
+ });
42
+
43
+ await it('a numeric column right-aligns + gets the numeric class', async () => {
44
+ const grid = makeGrid(BWA_COLUMNS, []);
45
+ const cells = grid.querySelectorAll('.adw-data-grid-row.header .adw-data-grid-cell');
46
+ expect(cells[0].classList.contains('align-start')).toBe(true);
47
+ expect(cells[1].classList.contains('align-end')).toBe(true);
48
+ expect(cells[1].classList.contains('numeric')).toBe(true);
49
+ grid.remove();
50
+ });
51
+
52
+ await it('honours an explicit align + monospace flag', async () => {
53
+ const grid = makeGrid(
54
+ [
55
+ { key: 'a', align: 'center' },
56
+ { key: 'b', monospace: true },
57
+ ],
58
+ [],
59
+ );
60
+ const cells = grid.querySelectorAll('.adw-data-grid-row.header .adw-data-grid-cell');
61
+ expect(cells[0].classList.contains('align-center')).toBe(true);
62
+ expect(cells[1].classList.contains('mono')).toBe(true);
63
+ grid.remove();
64
+ });
65
+
66
+ await it('derives grid-template-columns from width / flex', async () => {
67
+ const grid = makeGrid(
68
+ [
69
+ { key: 'a', flex: 2 },
70
+ { key: 'b', width: '80px' },
71
+ ],
72
+ [],
73
+ );
74
+ const table = grid.querySelector('.adw-data-grid-table') as HTMLElement;
75
+ expect(table.style.gridTemplateColumns).toBe('2fr 80px');
76
+ grid.remove();
77
+ });
78
+
79
+ await it('defaults the first column to flexible so amounts hug the right edge', async () => {
80
+ const grid = makeGrid(BWA_COLUMNS, []);
81
+ const table = grid.querySelector('.adw-data-grid-table') as HTMLElement;
82
+ // The CSSOM serialises the track back as `minmax(0px, 1fr)`.
83
+ expect(table.style.gridTemplateColumns).toBe('minmax(0px, 1fr) auto');
84
+ grid.remove();
85
+ });
86
+
87
+ await it('parses columns + rows from the JSON attributes', async () => {
88
+ const grid = document.createElement('adw-data-grid') as AdwDataGrid;
89
+ grid.setAttribute('columns', '[{"key":"a","label":"A"},{"key":"b","numeric":true}]');
90
+ grid.setAttribute('rows', '[{"a":"x","b":"1,00 €"}]');
91
+ document.body.appendChild(grid);
92
+ expect(grid.columns.length).toBe(2);
93
+ expect(grid.rows.length).toBe(1);
94
+ const bodyCells = grid.querySelectorAll('.adw-data-grid-row:not(.header) .adw-data-grid-cell');
95
+ expect(bodyCells[0].textContent).toBe('x');
96
+ expect(bodyCells[1].textContent).toBe('1,00 €');
97
+ grid.remove();
98
+ });
99
+ });
100
+
101
+ await describe('adw-data-grid rows', async () => {
102
+ await it('renders a row + a cell per column with the pre-formatted string', async () => {
103
+ const grid = makeGrid(BWA_COLUMNS, [
104
+ { position: 'Umsatzerlöse', total: '120.000,00 €' },
105
+ { position: 'Sonstige Erträge', total: '5.000,00 €' },
106
+ ]);
107
+ const rows = grid.querySelectorAll('.adw-data-grid-row:not(.header)');
108
+ expect(rows.length).toBe(2);
109
+ const firstCells = rows[0].querySelectorAll('.adw-data-grid-cell');
110
+ expect(firstCells.length).toBe(2);
111
+ expect(firstCells[0].textContent).toBe('Umsatzerlöse');
112
+ expect(firstCells[1].textContent).toBe('120.000,00 €');
113
+ grid.remove();
114
+ });
115
+
116
+ await it('coerces a numeric cell value to a string', async () => {
117
+ const grid = makeGrid([{ key: 'qty', numeric: true }], [{ qty: 3 }]);
118
+ const cell = grid.querySelector('.adw-data-grid-row:not(.header) .adw-data-grid-cell');
119
+ expect(cell?.textContent).toBe('3');
120
+ grid.remove();
121
+ });
122
+
123
+ await it('renders an empty cell for a missing key', async () => {
124
+ const grid = makeGrid(BWA_COLUMNS, [{ position: 'Only label' }]);
125
+ const cells = grid.querySelectorAll('.adw-data-grid-row:not(.header) .adw-data-grid-cell');
126
+ expect(cells[1].textContent).toBe('');
127
+ grid.remove();
128
+ });
129
+ });
130
+
131
+ await describe('adw-data-grid variants', async () => {
132
+ await it('a section row spans all columns with the first column value', async () => {
133
+ const grid = makeGrid(BWA_COLUMNS, [{ variant: 'section', position: 'Betriebseinnahmen' }]);
134
+ const row = grid.querySelector('.adw-data-grid-row.variant-section') as HTMLElement;
135
+ expect(row).toBeTruthy();
136
+ const cells = row.querySelectorAll('.adw-data-grid-cell');
137
+ expect(cells.length).toBe(1);
138
+ expect(cells[0].textContent).toBe('Betriebseinnahmen');
139
+ expect((cells[0] as HTMLElement).style.gridColumn).toBe('1 / -1');
140
+ grid.remove();
141
+ });
142
+
143
+ await it('tags subtotal + total rows with their variant class', async () => {
144
+ const grid = makeGrid(BWA_COLUMNS, [
145
+ { variant: 'subtotal', position: 'Gesamtleistung', total: '125.000,00 €' },
146
+ { variant: 'total', position: 'Betriebsergebnis', total: '42.000,00 €' },
147
+ ]);
148
+ expect(grid.querySelector('.adw-data-grid-row.variant-subtotal')).toBeTruthy();
149
+ expect(grid.querySelector('.adw-data-grid-row.variant-total')).toBeTruthy();
150
+ // A subtotal / total keeps the full per-column layout (not spanning).
151
+ const subtotalCells = grid.querySelectorAll('.variant-subtotal .adw-data-grid-cell');
152
+ expect(subtotalCells.length).toBe(2);
153
+ grid.remove();
154
+ });
155
+
156
+ await it('an unknown / absent variant falls back to normal', async () => {
157
+ const grid = makeGrid(BWA_COLUMNS, [
158
+ { position: 'x' },
159
+ { variant: 'bogus', position: 'y' } as unknown as AdwDataGridRow,
160
+ ]);
161
+ const normals = grid.querySelectorAll('.adw-data-grid-row.variant-normal');
162
+ expect(normals.length).toBe(2);
163
+ grid.remove();
164
+ });
165
+ });
166
+
167
+ await describe('adw-data-grid row-activated', async () => {
168
+ await it('element-level interactive makes normal rows clickable + fires {index,row}', async () => {
169
+ const rows: AdwDataGridRow[] = [
170
+ { position: 'Row A', total: '1,00 €' },
171
+ { position: 'Row B', total: '2,00 €' },
172
+ ];
173
+ const grid = makeGrid(BWA_COLUMNS, rows);
174
+ grid.interactive = true;
175
+ let detail: { index: number; row: AdwDataGridRow } | null = null;
176
+ grid.addEventListener('row-activated', (e) => {
177
+ detail = (e as CustomEvent).detail;
178
+ });
179
+ const interactiveRows = grid.querySelectorAll('.adw-data-grid-row.interactive');
180
+ expect(interactiveRows.length).toBe(2);
181
+ (interactiveRows[1] as HTMLElement).click();
182
+ expect(detail).toBeTruthy();
183
+ expect(detail!.index).toBe(1);
184
+ expect(detail!.row.position).toBe('Row B');
185
+ grid.remove();
186
+ });
187
+
188
+ await it('section / subtotal / total rows never activate', async () => {
189
+ const grid = makeGrid(BWA_COLUMNS, [
190
+ { variant: 'section', position: 'Sec' },
191
+ { variant: 'subtotal', position: 'Sub', total: '1,00 €' },
192
+ { variant: 'total', position: 'Tot', total: '2,00 €' },
193
+ { position: 'Data', total: '3,00 €' },
194
+ ]);
195
+ grid.interactive = true;
196
+ expect(grid.querySelectorAll('.adw-data-grid-row.interactive').length).toBe(1);
197
+ grid.remove();
198
+ });
199
+
200
+ await it('a per-row interactive flag opts a single row in / out', async () => {
201
+ const grid = makeGrid(BWA_COLUMNS, [
202
+ { position: 'clickable', interactive: true },
203
+ { position: 'not clickable' },
204
+ ]);
205
+ // No element-level flag: only the opted-in row is interactive.
206
+ const interactiveRows = grid.querySelectorAll('.adw-data-grid-row.interactive');
207
+ expect(interactiveRows.length).toBe(1);
208
+ expect(interactiveRows[0].querySelector('.adw-data-grid-cell')?.textContent).toBe('clickable');
209
+ grid.remove();
210
+ });
211
+
212
+ await it('a per-row interactive:false opts out even when the element is interactive', async () => {
213
+ const grid = makeGrid(BWA_COLUMNS, [{ position: 'a' }, { position: 'b', interactive: false }]);
214
+ grid.interactive = true;
215
+ expect(grid.querySelectorAll('.adw-data-grid-row.interactive').length).toBe(1);
216
+ grid.remove();
217
+ });
218
+
219
+ await it('Enter activates a focused interactive row', async () => {
220
+ const grid = makeGrid(BWA_COLUMNS, [{ position: 'Row A', total: '1,00 €' }]);
221
+ grid.interactive = true;
222
+ let fired = 0;
223
+ grid.addEventListener('row-activated', () => fired++);
224
+ const row = grid.querySelector('.adw-data-grid-row.interactive') as HTMLElement;
225
+ expect(row.tabIndex).toBe(0);
226
+ row.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
227
+ expect(fired).toBe(1);
228
+ grid.remove();
229
+ });
230
+ });
231
+
232
+ await describe('adw-data-grid caption', async () => {
233
+ await it('shows the caption from the attribute + hides it when empty', async () => {
234
+ const grid = makeGrid(BWA_COLUMNS, []);
235
+ const caption = grid.querySelector('.adw-data-grid-caption') as HTMLElement;
236
+ expect(caption.hidden).toBe(true);
237
+ grid.caption = 'Betriebswirtschaftliche Auswertung';
238
+ expect(caption.hidden).toBe(false);
239
+ expect(caption.textContent).toBe('Betriebswirtschaftliche Auswertung');
240
+ grid.remove();
241
+ });
242
+ });
243
+ };