@gjsify/adwaita-web 0.18.0 → 0.20.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/adwaita-web",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "Adwaita/Libadwaita web components for browser targets",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -41,13 +41,13 @@
41
41
  "@codemirror/language": "^6.12.4",
42
42
  "@codemirror/state": "^6.7.0",
43
43
  "@codemirror/view": "^6.43.4",
44
- "@gjsify/adwaita-fonts": "^0.18.0",
45
- "@gjsify/adwaita-icons": "^0.18.0",
44
+ "@gjsify/adwaita-fonts": "^0.20.0",
45
+ "@gjsify/adwaita-icons": "^0.20.0",
46
46
  "@lezer/highlight": "^1.2.3"
47
47
  },
48
48
  "devDependencies": {
49
- "@gjsify/cli": "^0.18.0",
50
- "@gjsify/unit": "^0.18.0",
49
+ "@gjsify/cli": "^0.20.0",
50
+ "@gjsify/unit": "^0.20.0",
51
51
  "sass": "^1.101.0",
52
52
  "typescript": "^6.0.3"
53
53
  },
@@ -17,18 +17,22 @@ adw-tab-view {
17
17
  color: var(--window-fg-color);
18
18
 
19
19
  // --- Tab bar (the AdwTabBar strip) ---
20
+ // Faithful port of libadwaita's `tabbar` (refs/libadwaita _tab-view.scss +
21
+ // _colors.scss): a headerbar-colored box with an inset bottom shade; tabs
22
+ // FILL their slot as rounded rects and use the currentColor alpha fills —
23
+ // selected 10% (hover 13%, active 19%), plain hover 7%, active 16%.
20
24
  .adw-tab-bar {
21
25
  display: flex;
22
- align-items: flex-end;
26
+ align-items: stretch;
23
27
  flex-shrink: 0;
24
28
  box-sizing: border-box;
25
- padding: 0 var(--spacing-xs);
29
+ padding: 0 1px 1px;
26
30
  background-color: var(--headerbar-bg-color);
27
31
  color: var(--headerbar-fg-color);
28
32
  // libadwaita's inset bottom shade under the bar.
29
33
  box-shadow: inset 0 -1px var(--headerbar-shade-color);
30
34
  overflow-x: auto;
31
- scrollbar-width: thin;
35
+ scrollbar-width: none;
32
36
 
33
37
  &[hidden] {
34
38
  display: none;
@@ -38,65 +42,84 @@ adw-tab-view {
38
42
  .adw-tab-box {
39
43
  display: flex;
40
44
  align-items: stretch;
41
- gap: var(--spacing-xs);
42
- padding: var(--spacing-xs) 0;
43
- min-height: 34px;
45
+ flex: 1 1 auto;
46
+ padding: 4px 0;
44
47
  }
45
48
 
46
- // --- A single tab chip ---
49
+ // --- A single tab ---
47
50
  .adw-tab {
51
+ position: relative;
48
52
  box-sizing: border-box;
49
53
  display: inline-flex;
50
54
  align-items: center;
55
+ justify-content: center;
51
56
  gap: var(--spacing-xs);
52
57
  max-width: 200px;
53
- min-height: 26px;
58
+ min-height: 30px;
54
59
  margin: 0;
55
- padding: 4px var(--spacing-s);
60
+ padding: 0 var(--spacing-m);
56
61
  border: none;
57
62
  border-radius: var(--button-radius);
58
63
  background-color: transparent;
59
- color: var(--headerbar-fg-color);
64
+ color: inherit;
60
65
  font-family: var(--font-family);
61
66
  font-size: var(--font-size-base);
62
- font-weight: 700;
63
67
  line-height: 1;
64
68
  cursor: pointer;
65
69
  user-select: none;
66
70
  -webkit-user-select: none;
67
- transition:
68
- background-color 150ms ease-in-out,
69
- color 150ms ease-in-out,
70
- box-shadow 150ms ease-in-out;
71
+ transition: background-color 150ms ease-in-out;
71
72
 
72
73
  &:hover {
73
- background-color: var(--button-hover-color);
74
+ background-color: color-mix(in srgb, currentColor 7%, transparent);
74
75
  }
75
76
 
76
77
  &:active {
77
- background-color: var(--button-active-color, var(--button-hover-color));
78
+ background-color: color-mix(in srgb, currentColor 16%, transparent);
78
79
  }
79
80
 
80
81
  &:focus-visible {
81
82
  outline: 2px solid var(--accent-color);
82
- outline-offset: -1px;
83
+ outline-offset: -2px;
83
84
  }
84
85
 
85
- // The selected tab — a raised chip on the view background, the way
86
- // libadwaita lifts the active tab off the bar.
86
+ // The selected tab — libadwaita's currentColor alpha fill covering the
87
+ // whole tab slot (NOT a raised chip on the view background).
87
88
  &.active {
88
- background-color: var(--view-bg-color);
89
- color: var(--view-fg-color);
90
- box-shadow:
91
- 0 1px 3px 1px rgba(0, 0, 6, 0.07),
92
- 0 2px 6px 2px rgba(0, 0, 6, 0.03);
89
+ background-color: color-mix(in srgb, currentColor 10%, transparent);
93
90
 
94
91
  &:hover {
95
- background-color: var(--view-bg-color);
92
+ background-color: color-mix(in srgb, currentColor 13%, transparent);
93
+ }
94
+
95
+ &:active {
96
+ background-color: color-mix(in srgb, currentColor 19%, transparent);
96
97
  }
97
98
  }
98
99
  }
99
100
 
101
+ // Thin separators between tabs, hidden next to the selected or hovered tab
102
+ // (libadwaita tabbox > separator + its .hidden state).
103
+ .adw-tab + .adw-tab::before {
104
+ content: '';
105
+ position: absolute;
106
+ left: 0;
107
+ top: 3px;
108
+ bottom: 3px;
109
+ width: 1px;
110
+ background-color: currentColor;
111
+ opacity: 0.2;
112
+ transition: opacity 150ms ease-in-out;
113
+ pointer-events: none;
114
+ }
115
+
116
+ .adw-tab.active + .adw-tab::before,
117
+ .adw-tab + .adw-tab.active::before,
118
+ .adw-tab:hover + .adw-tab::before,
119
+ .adw-tab + .adw-tab:hover::before {
120
+ opacity: 0;
121
+ }
122
+
100
123
  .adw-tab-title {
101
124
  white-space: nowrap;
102
125
  overflow: hidden;
@@ -154,15 +177,25 @@ adw-tab-view {
154
177
  }
155
178
  }
156
179
 
157
- // Single-tab bars dim the close affordance (libadwaita keeps a lone tab
180
+ // Adw.TabBar `expand-tabs` — tabs stretch to fill the bar evenly.
181
+ &[expand-tabs] .adw-tab {
182
+ flex: 1 1 0;
183
+ max-width: none;
184
+ }
185
+
186
+ // Web-specific escape hatch: static tab sets (e.g. documentation command
187
+ // tabs) render no close affordance.
188
+ &[no-close] .adw-tab-close {
189
+ display: none;
190
+ }
191
+
192
+ // Single-tab bars draw no tab background (libadwaita keeps a lone tab
158
193
  // non-interactive-looking).
159
194
  .adw-tab-bar.single-tab .adw-tab {
160
195
  &,
161
196
  &:hover,
162
197
  &:active {
163
198
  background: none;
164
- box-shadow: none;
165
- color: var(--headerbar-fg-color);
166
199
  }
167
200
  }
168
201
 
@@ -0,0 +1,108 @@
1
+ // DOM-level behaviour tests for <adw-tab-view>. Runs in a real browser via the
2
+ // @gjsify/adwaita-web browser test axis (the entry self-applies the compiled
3
+ // stylesheet, so computed-style assertions are valid). Asserts page building,
4
+ // selected↔click sync, notify/close events, and the expand-tabs / no-close
5
+ // attributes.
6
+ import { describe, expect, it } from '@gjsify/unit';
7
+
8
+ import type { AdwTabView } from './elements/adw-tab-view.js';
9
+
10
+ function makeTabView(attrs = ''): AdwTabView {
11
+ const host = document.createElement('div');
12
+ host.innerHTML = `<adw-tab-view ${attrs}>
13
+ <adw-tab-page title="One"><p>first</p></adw-tab-page>
14
+ <adw-tab-page title="Two"><p>second</p></adw-tab-page>
15
+ <adw-tab-page title="Three"><p>third</p></adw-tab-page>
16
+ </adw-tab-view>`;
17
+ document.body.appendChild(host);
18
+ return host.querySelector('adw-tab-view') as AdwTabView;
19
+ }
20
+
21
+ export const AdwTabViewTest = async () => {
22
+ await describe('adw-tab-view pages', async () => {
23
+ await it('builds one tab per page and shows the first page', async () => {
24
+ const view = makeTabView();
25
+ expect(view.querySelectorAll('.adw-tab').length).toBe(3);
26
+ expect(view.selected).toBe(0);
27
+ const pages = view.querySelectorAll('.adw-tab-page');
28
+ expect((pages[0] as HTMLElement).hidden).toBe(false);
29
+ expect((pages[1] as HTMLElement).hidden).toBe(true);
30
+ view.parentElement?.remove();
31
+ });
32
+
33
+ await it('clicking a tab selects its page and notifies', async () => {
34
+ const view = makeTabView();
35
+ let notified = -1;
36
+ view.addEventListener('notify::selected-page', (event) => {
37
+ notified = (event as CustomEvent).detail.selected;
38
+ });
39
+ (view.querySelectorAll('.adw-tab')[2] as HTMLButtonElement).click();
40
+ expect(view.selected).toBe(2);
41
+ expect(notified).toBe(2);
42
+ expect((view.querySelectorAll('.adw-tab-page')[2] as HTMLElement).hidden).toBe(false);
43
+ view.parentElement?.remove();
44
+ });
45
+
46
+ await it('setting the selected attribute switches pages', async () => {
47
+ const view = makeTabView();
48
+ view.setAttribute('selected', '1');
49
+ expect((view.querySelectorAll('.adw-tab-page')[1] as HTMLElement).hidden).toBe(false);
50
+ expect((view.querySelectorAll('.adw-tab-page')[0] as HTMLElement).hidden).toBe(true);
51
+ view.parentElement?.remove();
52
+ });
53
+
54
+ await it('emits close-page from the close affordance', async () => {
55
+ const view = makeTabView();
56
+ let closed = -1;
57
+ view.addEventListener('close-page', (event) => {
58
+ closed = (event as CustomEvent).detail.index;
59
+ });
60
+ (view.querySelectorAll('.adw-tab-close')[1] as HTMLButtonElement).click();
61
+ expect(closed).toBe(1);
62
+ // Closing must not also select the tab.
63
+ expect(view.selected).toBe(0);
64
+ view.parentElement?.remove();
65
+ });
66
+ });
67
+
68
+ await describe('adw-tab-view expand-tabs / no-close', async () => {
69
+ await it('no-close hides every close affordance', async () => {
70
+ const view = makeTabView('no-close');
71
+ expect(view.noClose).toBe(true);
72
+ for (const close of view.querySelectorAll('.adw-tab-close')) {
73
+ expect(getComputedStyle(close).display).toBe('none');
74
+ }
75
+ view.parentElement?.remove();
76
+ });
77
+
78
+ await it('expand-tabs stretches tabs evenly across the bar', async () => {
79
+ const view = makeTabView('expand-tabs');
80
+ expect(view.expandTabs).toBe(true);
81
+ for (const tab of view.querySelectorAll('.adw-tab')) {
82
+ expect(getComputedStyle(tab).flexGrow).toBe('1');
83
+ }
84
+ view.parentElement?.remove();
85
+ });
86
+
87
+ await it('expand-tabs draws separators, hidden next to the active tab', async () => {
88
+ const view = makeTabView('expand-tabs');
89
+ const tabs = view.querySelectorAll('.adw-tab');
90
+ // Tab 0 is active: the separator before tab 1 is hidden, the one
91
+ // before tab 2 (between two inactive tabs) is visible.
92
+ expect(getComputedStyle(tabs[1], '::before').opacity).toBe('0');
93
+ expect(getComputedStyle(tabs[2], '::before').opacity).toBe('0.2');
94
+ view.parentElement?.remove();
95
+ });
96
+
97
+ await it('properties reflect to attributes', async () => {
98
+ const view = makeTabView();
99
+ view.noClose = true;
100
+ view.expandTabs = true;
101
+ expect(view.hasAttribute('no-close')).toBe(true);
102
+ expect(view.hasAttribute('expand-tabs')).toBe(true);
103
+ view.noClose = false;
104
+ expect(view.hasAttribute('no-close')).toBe(false);
105
+ view.parentElement?.remove();
106
+ });
107
+ });
108
+ };
@@ -9,6 +9,10 @@
9
9
  // selected (zero-based index of the visible page, default 0)
10
10
  // autohide (boolean — hide the tab bar when only one page remains, mirroring
11
11
  // the Adw.TabBar `autohide` property)
12
+ // expand-tabs (boolean — tabs stretch to fill the bar evenly, mirroring the
13
+ // Adw.TabBar `expand-tabs` property)
14
+ // no-close (boolean — render no close affordance; web-specific escape hatch
15
+ // for static tab sets such as documentation command tabs)
12
16
  // Events:
13
17
  // `notify::selected-page` (CustomEvent, bubbles, `detail = { selected }`) when
14
18
  // the visible page changes — mirrors the Adw.TabView `selected-page` property.
@@ -37,7 +41,7 @@ export class AdwTabView extends HTMLElement {
37
41
  private _initialized = false;
38
42
 
39
43
  static get observedAttributes() {
40
- return ['selected', 'autohide'];
44
+ return ['selected', 'autohide', 'expand-tabs', 'no-close'];
41
45
  }
42
46
 
43
47
  /** Zero-based index of the visible page. */
@@ -59,6 +63,26 @@ export class AdwTabView extends HTMLElement {
59
63
  else this.removeAttribute('autohide');
60
64
  }
61
65
 
66
+ /** Whether tabs stretch to fill the bar evenly (Adw.TabBar `expand-tabs`). */
67
+ get expandTabs(): boolean {
68
+ return this.hasAttribute('expand-tabs');
69
+ }
70
+
71
+ set expandTabs(value: boolean) {
72
+ if (value) this.setAttribute('expand-tabs', '');
73
+ else this.removeAttribute('expand-tabs');
74
+ }
75
+
76
+ /** Whether the close affordance is omitted (static tab sets). */
77
+ get noClose(): boolean {
78
+ return this.hasAttribute('no-close');
79
+ }
80
+
81
+ set noClose(value: boolean) {
82
+ if (value) this.setAttribute('no-close', '');
83
+ else this.removeAttribute('no-close');
84
+ }
85
+
62
86
  connectedCallback() {
63
87
  if (this._initialized) return;
64
88
  this._initialized = true;