@gjsify/adwaita-web 0.51.0 → 0.52.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 (103) hide show
  1. package/dist/adwaita-web.css +255 -27
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/blueprint-layout.spec.d.ts +1 -0
  4. package/lib/types/blueprint-markup.spec.d.ts +4 -0
  5. package/lib/types/blueprint-tree.spec.d.ts +1 -0
  6. package/lib/types/elements/adw-bottom-sheet.d.ts +31 -13
  7. package/lib/types/elements/adw-carousel.d.ts +5 -0
  8. package/lib/types/elements/adw-header-bar.d.ts +15 -0
  9. package/lib/types/elements/adw-inline-view-switcher.d.ts +1 -0
  10. package/lib/types/elements/adw-navigation-view.d.ts +1 -0
  11. package/lib/types/elements/adw-sidebar.d.ts +10 -0
  12. package/lib/types/elements/adw-tab-view.d.ts +1 -0
  13. package/lib/types/elements/adw-toggle-group.d.ts +11 -0
  14. package/lib/types/elements/gtk-action-bar.d.ts +19 -0
  15. package/lib/types/elements/gtk-box.d.ts +17 -0
  16. package/lib/types/elements/gtk-button.d.ts +2 -0
  17. package/lib/types/elements/gtk-label.d.ts +122 -0
  18. package/lib/types/gio-menu.spec.d.ts +1 -0
  19. package/lib/types/gtk-box.spec.d.ts +1 -0
  20. package/lib/types/gtk-label.spec.d.ts +1 -0
  21. package/lib/types/gtk-value-doors.spec.d.ts +1 -0
  22. package/lib/types/index.d.ts +4 -1
  23. package/lib/types/namespace/gio.d.ts +1 -0
  24. package/lib/types/namespace/gtk.d.ts +5 -0
  25. package/lib/types/shared-tree-builder.d.ts +64 -0
  26. package/lib/types/source-view/asm6502.d.ts +1 -20
  27. package/lib/types/source-view/index.d.ts +3 -2
  28. package/lib/types/source-view/theme.d.ts +1 -1
  29. package/lib/types/string-list-slot.d.ts +4 -0
  30. package/lib/types/styles.generated.d.ts +1 -1
  31. package/lib/types/tags.spec.d.ts +1 -0
  32. package/lib/types/value-lists.spec.d.ts +1 -0
  33. package/package.json +8 -6
  34. package/scss/_action_bar.scss +51 -0
  35. package/scss/_banner.scss +8 -1
  36. package/scss/_bottom_sheet.scss +57 -0
  37. package/scss/_box.scss +23 -0
  38. package/scss/_button.scss +29 -7
  39. package/scss/_button_content.scss +6 -1
  40. package/scss/_button_row.scss +5 -2
  41. package/scss/_carousel.scss +6 -0
  42. package/scss/_labels.scss +126 -12
  43. package/scss/_navigation_split_view.scss +18 -0
  44. package/scss/_overlay_split_view.scss +5 -0
  45. package/scss/_reset.scss +29 -4
  46. package/scss/_split_button.scss +5 -2
  47. package/scss/_status_page.scss +31 -17
  48. package/scss/_theme.scss +11 -3
  49. package/scss/_toolbar_view.scss +10 -0
  50. package/scss/_variables.scss +1 -0
  51. package/scss/_widget.scss +55 -0
  52. package/scss/_window_title.scss +9 -0
  53. package/scss/adwaita-skin.scss +8 -2
  54. package/src/adw-banner.spec.ts +16 -0
  55. package/src/adw-button-row.spec.ts +8 -0
  56. package/src/adw-header-bar.spec.ts +17 -0
  57. package/src/blueprint-layout.spec.ts +223 -0
  58. package/src/blueprint-markup.spec.ts +155 -0
  59. package/src/blueprint-tree.spec.ts +174 -0
  60. package/src/bottom-sheet.spec.ts +166 -1
  61. package/src/chrome.spec.ts +5 -2
  62. package/src/elements/adw-bottom-sheet.ts +166 -59
  63. package/src/elements/adw-carousel.ts +10 -5
  64. package/src/elements/adw-clamp.ts +12 -0
  65. package/src/elements/adw-combo-row.ts +13 -4
  66. package/src/elements/adw-header-bar.ts +46 -3
  67. package/src/elements/adw-inline-view-switcher.ts +11 -0
  68. package/src/elements/adw-navigation-view.ts +10 -0
  69. package/src/elements/adw-sidebar.ts +22 -0
  70. package/src/elements/adw-spin-row.ts +44 -5
  71. package/src/elements/adw-status-page.ts +12 -6
  72. package/src/elements/adw-tab-view.ts +20 -3
  73. package/src/elements/adw-toggle-group.ts +45 -4
  74. package/src/elements/adw-toolbar-view.ts +9 -0
  75. package/src/elements/gtk-action-bar.ts +90 -0
  76. package/src/elements/gtk-box.ts +87 -0
  77. package/src/elements/gtk-button.ts +60 -13
  78. package/src/elements/gtk-drop-down.ts +10 -1
  79. package/src/elements/gtk-label.ts +415 -0
  80. package/src/gio-menu.spec.ts +125 -0
  81. package/src/globals.d.ts +7 -0
  82. package/src/gtk-box.spec.ts +168 -0
  83. package/src/gtk-button.spec.ts +123 -1
  84. package/src/gtk-label.spec.ts +595 -0
  85. package/src/gtk-value-doors.spec.ts +104 -0
  86. package/src/index.ts +14 -1
  87. package/src/keyboard-operable.spec.ts +2 -1
  88. package/src/namespace/gio.ts +19 -0
  89. package/src/namespace/gtk.ts +14 -0
  90. package/src/shared-tree-builder.ts +268 -0
  91. package/src/shared-trees.spec.ts +296 -32
  92. package/src/source-view/adw-source-view.ts +3 -3
  93. package/src/source-view/asm6502.ts +11 -191
  94. package/src/source-view/index.ts +17 -11
  95. package/src/source-view/theme.ts +16 -16
  96. package/src/split-button.spec.ts +7 -0
  97. package/src/string-list-slot.ts +21 -0
  98. package/src/styles.generated.ts +1 -1
  99. package/src/tags.spec.ts +51 -0
  100. package/src/test.browser.mts +18 -0
  101. package/src/value-lists.spec.ts +93 -0
  102. package/lib/types/source-view/hex.d.ts +0 -21
  103. package/src/source-view/hex.ts +0 -37
@@ -0,0 +1,168 @@
1
+ // DOM-level tests for <gtk-box>, read off LAYOUT rather than the element's own state: an
2
+ // element asserting its bookkeeping agrees with itself while the page is wrong.
3
+ //
4
+ // The last case is the reason the element exists — an authored tree with a `Gtk.Box` of
5
+ // `Gtk.Label`s, mounted through the shipped `mountSharedTree`, was an unknown inline element
6
+ // holding two unknown empty ones. It is a `SharedTreeNode` literal, the shape a `.blp`'s
7
+ // `?shared-tree` projection hands over, so the case holds without the build seam.
8
+
9
+ import { describe, expect, it } from '@gjsify/unit';
10
+
11
+ import { BOX_ORIENTATION_VECTORS, BOX_SPACING_VECTORS, type SharedTreeNode } from '@gjsify/adwaita-core/conformance';
12
+
13
+ import { Gtk } from '@gjsify/adwaita-web';
14
+ import { mountSharedTree } from './shared-tree-builder.js';
15
+
16
+ import type { GtkBox } from './elements/gtk-box.js';
17
+
18
+ /** A box with `count` fixed-size children, in a 400px-wide host. */
19
+ function mount(count = 3): { box: GtkBox; kids: HTMLElement[]; host: HTMLElement } {
20
+ const host = document.createElement('div');
21
+ host.style.width = '400px';
22
+ document.body.appendChild(host);
23
+ const box = document.createElement('gtk-box') as GtkBox;
24
+ const kids: HTMLElement[] = [];
25
+ for (let i = 0; i < count; i++) {
26
+ const kid = document.createElement('div');
27
+ kid.style.width = `${20 + i * 10}px`;
28
+ kid.style.height = '10px';
29
+ box.appendChild(kid);
30
+ kids.push(kid);
31
+ }
32
+ host.appendChild(box);
33
+ return { box, kids, host };
34
+ }
35
+
36
+ const rect = (el: Element) => el.getBoundingClientRect();
37
+
38
+ /** Author `value` the way markup does: absent is no attribute at all. */
39
+ function author(el: HTMLElement, name: string, value: string | number | null): void {
40
+ if (value === null) el.removeAttribute(name);
41
+ else el.setAttribute(name, String(value));
42
+ }
43
+
44
+ export const GtkBoxTest = async () => {
45
+ await describe('<gtk-box> against BOX_SPACING_VECTORS', async () => {
46
+ for (const vector of BOX_SPACING_VECTORS) {
47
+ await it(vector.rule, () => {
48
+ const { box, host } = mount(0);
49
+ author(box, 'spacing', vector.value);
50
+ expect(box.spacing).toBe(vector.spacing);
51
+ expect(getComputedStyle(box).columnGap).toBe(`${vector.spacing}px`);
52
+ host.remove();
53
+ });
54
+ }
55
+ });
56
+
57
+ await describe('<gtk-box> against BOX_ORIENTATION_VECTORS', async () => {
58
+ for (const vector of BOX_ORIENTATION_VECTORS) {
59
+ await it(vector.rule, () => {
60
+ const { box, host } = mount(0);
61
+ author(box, 'orientation', vector.value);
62
+ expect(box.orientation).toBe(vector.orientation);
63
+ expect(getComputedStyle(box).flexDirection).toBe(vector.orientation === 'vertical' ? 'column' : 'row');
64
+ host.remove();
65
+ });
66
+ }
67
+ });
68
+
69
+ await describe('<gtk-box> orientation', async () => {
70
+ await it('lays children out in a row by default, as GtkOrientable does', () => {
71
+ const { box, kids, host } = mount();
72
+ expect(box.orientation).toBe('horizontal');
73
+ expect(rect(kids[1]).left).toBe(rect(kids[0]).right);
74
+ expect(rect(kids[1]).top).toBe(rect(kids[0]).top);
75
+ host.remove();
76
+ });
77
+
78
+ await it('stacks them in a column when vertical, each child taking the full width', () => {
79
+ const { box, kids, host } = mount();
80
+ box.orientation = 'vertical';
81
+ expect(rect(kids[1]).top).toBe(rect(kids[0]).bottom);
82
+ expect(rect(kids[0]).left).toBe(rect(kids[1]).left);
83
+ host.remove();
84
+ });
85
+ });
86
+
87
+ await describe('<gtk-box> spacing', async () => {
88
+ await it('puts the gap BETWEEN children only, never at the edges', () => {
89
+ const { box, kids, host } = mount();
90
+ box.spacing = 12;
91
+ expect(rect(kids[0]).left).toBe(rect(box).left);
92
+ expect(rect(kids[1]).left - rect(kids[0]).right).toBe(12);
93
+ expect(rect(kids[2]).left - rect(kids[1]).right).toBe(12);
94
+ host.remove();
95
+ });
96
+
97
+ await it('runs along the column when vertical', () => {
98
+ const { box, kids, host } = mount();
99
+ box.orientation = 'vertical';
100
+ box.setAttribute('spacing', '6');
101
+ expect(rect(kids[1]).top - rect(kids[0]).bottom).toBe(6);
102
+ host.remove();
103
+ });
104
+ });
105
+
106
+ await describe('<gtk-box> homogeneous', async () => {
107
+ await it('gives every child the same share of the axis', () => {
108
+ const { box, kids, host } = mount();
109
+ box.homogeneous = true;
110
+ const widths = kids.map((kid) => Math.round(rect(kid).width));
111
+ expect(widths[0]).toBe(widths[1]);
112
+ expect(widths[1]).toBe(widths[2]);
113
+ expect(widths[0]).toBeGreaterThan(100);
114
+ host.remove();
115
+ });
116
+ });
117
+
118
+ await describe('<gtk-box> notify', async () => {
119
+ await it('fires notify:: on a REAL change only, with the normalised value', () => {
120
+ const { box, host } = mount(0);
121
+ const seen: unknown[] = [];
122
+ box.addEventListener('notify::spacing', (event) => seen.push((event as CustomEvent).detail.spacing));
123
+ box.spacing = 4;
124
+ box.setAttribute('spacing', '4.0');
125
+ box.spacing = -1;
126
+ expect(seen.join(',')).toBe('4,0');
127
+ host.remove();
128
+ });
129
+ });
130
+
131
+ await describe('<gtk-box> as the Gtk.Box of an authored tree', async () => {
132
+ const tree: SharedTreeNode = {
133
+ tag: 'GtkBox',
134
+ props: { orientation: 'vertical', spacing: 18 },
135
+ children: [
136
+ { tag: 'GtkLabel', props: { label: 'Effect services' }, styleClasses: ['title-1'] },
137
+ { tag: 'GtkLabel', props: { label: 'effect/FileSystem over Gio.File', xalign: 0 } },
138
+ ],
139
+ };
140
+
141
+ await it('mounts upgraded elements, stacked by the authored orientation and spacing', () => {
142
+ const { root, unmount } = mountSharedTree(tree);
143
+ try {
144
+ expect(root instanceof Gtk.Box).toBe(true);
145
+ const labels = [...root.children];
146
+ expect(labels.every((label) => label instanceof Gtk.Label)).toBe(true);
147
+ expect(getComputedStyle(root).flexDirection).toBe('column');
148
+ expect(Math.round(rect(labels[1]).top - rect(labels[0]).bottom)).toBe(18);
149
+ } finally {
150
+ unmount();
151
+ }
152
+ });
153
+
154
+ await it('renders the authored text on screen, and the style class it carries', () => {
155
+ const { root, unmount } = mountSharedTree(tree);
156
+ try {
157
+ const [title, subtitle] = [...root.children] as HTMLElement[];
158
+ expect(title.textContent).toBe('Effect services');
159
+ expect(subtitle.textContent).toBe('effect/FileSystem over Gio.File');
160
+ expect(rect(title).height).toBeGreaterThan(0);
161
+ expect(title.classList.contains('title-1')).toBe(true);
162
+ expect(getComputedStyle(title).fontWeight).toBe('800');
163
+ } finally {
164
+ unmount();
165
+ }
166
+ });
167
+ });
168
+ };
@@ -18,7 +18,8 @@ function mountWithStyles(names: readonly string[]): string[] {
18
18
  for (const name of names) el.setAttribute(name, '');
19
19
  document.body.appendChild(el);
20
20
  const btn = el.querySelector('button') as HTMLButtonElement;
21
- return Array.from(btn.classList).filter((cls) => cls !== 'adw-button');
21
+ // `adw-button` is the skin and `text-button` says what fills the button, not its style.
22
+ return Array.from(btn.classList).filter((cls) => cls !== 'adw-button' && cls !== 'text-button');
22
23
  }
23
24
 
24
25
  function unmountAll(): void {
@@ -55,5 +56,126 @@ export const GtkButtonTest = async () => {
55
56
  expect(btn.classList.contains('suggested-action')).toBe(true);
56
57
  unmountAll();
57
58
  });
59
+
60
+ // What `buildSharedTree` writes for `Gtk.Button { icon-name: …; styles ["flat"] }`:
61
+ // the GObject property as an attribute and the style as a host class. Read as
62
+ // `icon` and the boolean flags only, that button rendered empty and unstyled.
63
+ await it('a host class and icon-name, as a built .blp writes them, style the button', () => {
64
+ const el = document.createElement('gtk-button');
65
+ el.setAttribute('icon-name', 'go-previous-symbolic');
66
+ el.classList.add('flat', 'suggested-action');
67
+ document.body.appendChild(el);
68
+ const btn = el.querySelector('button') as HTMLButtonElement;
69
+ expect(Array.from(btn.classList).filter((cls) => cls !== 'adw-button')).toStrictEqual([
70
+ 'flat',
71
+ 'suggested-action',
72
+ 'icon-only',
73
+ ]);
74
+ expect(btn.querySelector('gtk-image')?.getAttribute('icon-name')).toBe('go-previous-symbolic');
75
+ el.classList.add('pill');
76
+ expect(btn.classList.contains('pill')).toBe(true);
77
+ unmountAll();
78
+ });
79
+ });
80
+
81
+ // `Gtk.Button:child`. A `.blp` writes `child: Adw.ButtonContent { … }`, which the shared
82
+ // tree carries as a child with `slot: 'child'`. The element used to wipe its children on
83
+ // connect and read their text as the label, so the button content vanished and
84
+ // `mountSharedTree` refused the slot as unknown.
85
+ await describe('<gtk-button> child', async () => {
86
+ await it('an authored child: fills the inner button in place of icon and label', () => {
87
+ const el = document.createElement('gtk-button');
88
+ el.classList.add('suggested-action', 'pill');
89
+ const content = document.createElement('adw-button-content');
90
+ content.setAttribute('slot', 'child');
91
+ content.setAttribute('label', 'Download');
92
+ content.setAttribute('icon-name', 'folder-download-symbolic');
93
+ el.appendChild(content);
94
+ document.body.appendChild(el);
95
+ const btn = el.querySelector('button') as HTMLButtonElement;
96
+ expect(Array.from(btn.children)).toStrictEqual([content]);
97
+ expect(Array.from(btn.childNodes).some((node) => node.nodeType === Node.TEXT_NODE)).toBe(false);
98
+ expect(btn.classList.contains('suggested-action') && btn.classList.contains('pill')).toBe(true);
99
+ // A style class added later rebuilds the button and keeps the child.
100
+ el.classList.add('flat');
101
+ expect(content.parentElement).toBe(btn);
102
+ unmountAll();
103
+ });
104
+
105
+ await it('a bare element child is the child too; inline text stays the label', async () => {
106
+ const late = document.createElement('gtk-button');
107
+ document.body.appendChild(late);
108
+ const content = document.createElement('adw-button-content');
109
+ late.appendChild(content);
110
+ const labelled = document.createElement('gtk-button');
111
+ labelled.textContent = 'Go';
112
+ document.body.appendChild(labelled);
113
+ // The late child reaches the button through the slot observer, one microtask on.
114
+ await Promise.resolve();
115
+ expect(content.parentElement).toBe(late.querySelector('button'));
116
+ expect(labelled.querySelector('button')?.textContent).toBe('Go');
117
+ unmountAll();
118
+ });
119
+ });
120
+
121
+ // libadwaita's `%pill_button` is `padding: 10px 32px` over a `min-height: 24px` content
122
+ // box, so a GTK pill is 44px tall. This one was 6px 20px and 34px, measured against a
123
+ // GTK render of the gallery's wrap box: every chip 10px short and 24px narrow.
124
+ await describe('<gtk-button pill> geometry', async () => {
125
+ await it('carries the GTK pill padding and height', () => {
126
+ const el = document.createElement('gtk-button');
127
+ el.setAttribute('label', 'Design');
128
+ el.setAttribute('pill', '');
129
+ document.body.appendChild(el);
130
+ const btn = el.querySelector('button') as HTMLButtonElement;
131
+ const style = getComputedStyle(btn);
132
+ expect([style.paddingTop, style.paddingRight, style.paddingBottom, style.paddingLeft]).toStrictEqual([
133
+ '10px',
134
+ '32px',
135
+ '10px',
136
+ '32px',
137
+ ]);
138
+ expect(btn.getBoundingClientRect().height).toBe(44);
139
+ unmountAll();
140
+ });
141
+
142
+ // Measured against a GTK render of the gallery's Gtk.Button and Adw.ButtonContent
143
+ // `.blp` files: each text button 6-7px narrower, the content pill 47px narrower
144
+ // with its label painted in the window colour, and "Delete" filled red.
145
+ await it('a label-only button carries the GTK text-button padding', () => {
146
+ const el = document.createElement('gtk-button');
147
+ el.setAttribute('label', 'Suggested');
148
+ document.body.appendChild(el);
149
+ const style = getComputedStyle(el.querySelector('button') as HTMLButtonElement);
150
+ expect([style.paddingLeft, style.paddingRight]).toStrictEqual(['17px', '17px']);
151
+ unmountAll();
152
+ });
153
+
154
+ await it('a pill keeps its padding around button content, which takes the button colour', () => {
155
+ const el = document.createElement('gtk-button');
156
+ el.classList.add('suggested-action', 'pill');
157
+ const content = document.createElement('adw-button-content');
158
+ content.setAttribute('label', 'Download');
159
+ el.appendChild(content);
160
+ document.body.appendChild(el);
161
+ const btn = el.querySelector('button') as HTMLButtonElement;
162
+ const style = getComputedStyle(btn);
163
+ expect([style.paddingLeft, style.paddingRight]).toStrictEqual(['32px', '32px']);
164
+ expect(getComputedStyle(content).color).toBe(style.color);
165
+ unmountAll();
166
+ });
167
+
168
+ await it('a destructive button is red text on a tint, not a filled button', () => {
169
+ const el = document.createElement('gtk-button');
170
+ el.setAttribute('label', 'Delete');
171
+ el.classList.add('destructive-action');
172
+ document.body.appendChild(el);
173
+ const style = getComputedStyle(el.querySelector('button') as HTMLButtonElement);
174
+ // The tint is the text colour at 15% alpha (`color-mix` computes to `color(srgb … / 0.15)`);
175
+ // a filled button is opaque and paints white text.
176
+ expect(style.color === 'rgb(255, 255, 255)').toBe(false);
177
+ expect(/[/,] 0\.15\)$/.test(style.backgroundColor)).toBe(true);
178
+ unmountAll();
179
+ });
58
180
  });
59
181
  };