@gjsify/adwaita-web 0.40.0 → 0.42.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 (113) hide show
  1. package/README.md +98 -3
  2. package/dist/adwaita-web.css +142 -63
  3. package/dist/adwaita-web.css.map +1 -1
  4. package/lib/types/adw-fonts.spec.d.ts +2 -0
  5. package/lib/types/breakpoints.d.ts +10 -1
  6. package/lib/types/connect-lifecycle.spec.d.ts +1 -0
  7. package/lib/types/elements/adw-about-dialog.d.ts +1 -0
  8. package/lib/types/elements/adw-action-row.d.ts +2 -0
  9. package/lib/types/elements/adw-alert-dialog.d.ts +14 -2
  10. package/lib/types/elements/adw-carousel.d.ts +48 -0
  11. package/lib/types/elements/adw-dialog.d.ts +2 -7
  12. package/lib/types/elements/adw-icon.d.ts +17 -1
  13. package/lib/types/elements/adw-navigation-split-view.d.ts +7 -0
  14. package/lib/types/elements/adw-overlay-split-view.d.ts +6 -0
  15. package/lib/types/elements/adw-popover.d.ts +1 -0
  16. package/lib/types/elements/adw-preferences-dialog.d.ts +1 -0
  17. package/lib/types/elements/adw-switch.d.ts +10 -0
  18. package/lib/types/elements/adw-tab-view.d.ts +23 -0
  19. package/lib/types/elements/adw-toggle-group.d.ts +2 -0
  20. package/lib/types/elements/adw-toolbar-view.d.ts +8 -0
  21. package/lib/types/elements/modal-surface.d.ts +59 -0
  22. package/lib/types/elements/roving-focus.d.ts +26 -0
  23. package/lib/types/elements/row-activation.d.ts +26 -0
  24. package/lib/types/empty-sections.d.ts +15 -0
  25. package/lib/types/empty-sections.spec.d.ts +1 -0
  26. package/lib/types/fonts.d.ts +1 -0
  27. package/lib/types/icon-registry.d.ts +35 -0
  28. package/lib/types/icon-registry.spec.d.ts +18 -0
  29. package/lib/types/index.d.ts +3 -1
  30. package/lib/types/keyboard-operable.spec.d.ts +1 -0
  31. package/lib/types/scroll-shading.d.ts +56 -0
  32. package/lib/types/styles.generated.d.ts +1 -1
  33. package/package.json +10 -6
  34. package/scss/_banner.scss +1 -1
  35. package/scss/_carousel.scss +15 -1
  36. package/scss/_checks.scss +8 -8
  37. package/scss/_combo_row.scss +1 -1
  38. package/scss/_drop_down.scss +1 -1
  39. package/scss/_expander_row.scss +1 -1
  40. package/scss/_headerbar.scss +35 -6
  41. package/scss/_icon.scss +47 -0
  42. package/scss/_icons.generated.scss +36 -6
  43. package/scss/_password_entry_row.scss +1 -1
  44. package/scss/_preferences_dialog.scss +3 -2
  45. package/scss/_progress_bar.scss +1 -1
  46. package/scss/_row.scss +1 -1
  47. package/scss/_scrolling.scss +107 -0
  48. package/scss/_status_page.scss +1 -1
  49. package/scss/_style_classes.scss +21 -8
  50. package/scss/_tab_view.scss +1 -1
  51. package/scss/_theme.scss +1 -0
  52. package/scss/_toast.scss +6 -24
  53. package/scss/_toolbar_view.scss +52 -22
  54. package/scss/_variables.scss +30 -3
  55. package/scss/_view_switcher.scss +80 -20
  56. package/scss/adwaita-skin.scss +1 -0
  57. package/src/adw-action-rows.spec.ts +21 -9
  58. package/src/adw-button-content.spec.ts +11 -6
  59. package/src/adw-carousel.spec.ts +141 -1
  60. package/src/adw-fonts.spec.ts +188 -0
  61. package/src/adw-header-bar.spec.ts +92 -0
  62. package/src/adw-icon.spec.ts +17 -8
  63. package/src/adw-row-state.spec.ts +11 -1
  64. package/src/adw-tab-view.spec.ts +151 -0
  65. package/src/bottom-sheet.spec.ts +87 -0
  66. package/src/breakpoints.ts +18 -4
  67. package/src/chrome.spec.ts +233 -0
  68. package/src/connect-lifecycle.spec.ts +351 -0
  69. package/src/elements/adw-about-dialog.ts +24 -18
  70. package/src/elements/adw-action-row.ts +14 -2
  71. package/src/elements/adw-alert-dialog.ts +39 -20
  72. package/src/elements/adw-button-row.ts +3 -0
  73. package/src/elements/adw-carousel.ts +121 -4
  74. package/src/elements/adw-clamp.ts +3 -2
  75. package/src/elements/adw-data-grid.ts +5 -2
  76. package/src/elements/adw-dialog.ts +16 -63
  77. package/src/elements/adw-expander-row.ts +18 -4
  78. package/src/elements/adw-header-bar.ts +2 -0
  79. package/src/elements/adw-icon.ts +32 -6
  80. package/src/elements/adw-inline-view-switcher.ts +12 -0
  81. package/src/elements/adw-menu-button.ts +17 -10
  82. package/src/elements/adw-navigation-split-view.ts +30 -7
  83. package/src/elements/adw-overlay-split-view.ts +15 -3
  84. package/src/elements/adw-popover.ts +11 -0
  85. package/src/elements/adw-preferences-dialog.ts +18 -16
  86. package/src/elements/adw-preferences-group.ts +7 -0
  87. package/src/elements/adw-sidebar.ts +15 -0
  88. package/src/elements/adw-status-page.ts +12 -4
  89. package/src/elements/adw-switch-row.ts +7 -0
  90. package/src/elements/adw-switch.ts +19 -1
  91. package/src/elements/adw-tab-view.ts +70 -7
  92. package/src/elements/adw-toast-overlay.ts +8 -1
  93. package/src/elements/adw-toggle-group.ts +94 -2
  94. package/src/elements/adw-toolbar-view.ts +28 -2
  95. package/src/elements/adw-view-switcher-bar.ts +18 -0
  96. package/src/elements/adw-view-switcher.ts +14 -0
  97. package/src/elements/modal-surface.ts +180 -0
  98. package/src/elements/roving-focus.ts +154 -0
  99. package/src/elements/row-activation.ts +92 -0
  100. package/src/empty-sections.spec.ts +81 -0
  101. package/src/empty-sections.ts +54 -0
  102. package/src/fonts.ts +25 -0
  103. package/src/icon-registry.spec.ts +240 -0
  104. package/src/icon-registry.ts +157 -0
  105. package/src/index.ts +30 -1
  106. package/src/keyboard-operable.spec.ts +755 -0
  107. package/src/scroll-shading.ts +255 -0
  108. package/src/source-view/theme.ts +15 -3
  109. package/src/split-button.spec.ts +6 -2
  110. package/src/style-classes.spec.ts +6 -0
  111. package/src/styles.generated.ts +1 -1
  112. package/src/test.browser.mts +14 -0
  113. package/src/view-switcher.spec.ts +149 -0
@@ -0,0 +1,255 @@
1
+ // Browser wiring for GtkScrolledWindow's edge indicators — the DOM half of
2
+ // `@gjsify/adwaita-core`'s `scrolling.ts`, which owns the arithmetic.
3
+ //
4
+ // WHY a discovery pass and not a widget. GTK has a `scrolledwindow` node, so the
5
+ // stylesheet can say `toolbarview.undershoot-top scrolledwindow` and be done. The
6
+ // browser has no such element — any block with `overflow: auto` is one — so the
7
+ // equivalent scope has to be computed. The scope is deliberately the same as
8
+ // upstream's: a DESCENDANT selector, so a toolbar view shades EVERY scroller under
9
+ // it, not only the one it directly wraps. That is what makes the storybook's three
10
+ // panes (story list, preview, controls) each shade themselves independently.
11
+ //
12
+ // WHY the top and bottom indicators share one pseudo-element each. Undershoot and
13
+ // overshoot at the same edge are mutually exclusive by construction: an undershoot
14
+ // says content is hidden past the edge, an overshoot says the position is past it,
15
+ // and `overshootDistance` is 0 for as long as `isScrolledFromStart` is true. One
16
+ // box per edge, switched by which state is live.
17
+ //
18
+ // WHY sticky. The indicator has to stay welded to the scrollport edge while the
19
+ // content moves under it, and it must consume no layout. `position: sticky` plus a
20
+ // negative margin equal to its height is the only combination that does both — an
21
+ // absolutely-positioned child of a scroll container scrolls away with the content.
22
+ // The one constraint this leaves: a scroller that is itself a flex/grid container
23
+ // with a `gap` pays that gap once for the pseudo-element. Scrollers are wrappers,
24
+ // so this has not bitten; a scroller with a gap should move it to an inner box.
25
+ //
26
+ // Reference: refs/gtk/gtk/gtkscrolledwindow.c
27
+ // Reference: refs/libadwaita/src/stylesheet/widgets/_scrolling.scss
28
+ // Copyright (c) GNOME contributors (GTK, libadwaita). LGPLv2.1+.
29
+
30
+ import {
31
+ ADW_OVERSHOOT_SETTLE_MS,
32
+ ADW_UNDERSHOOT_CLASSES,
33
+ accumulateOvershoot,
34
+ overshootDistance,
35
+ type ScrollAdjustment,
36
+ scrollUndershootClasses,
37
+ } from '@gjsify/adwaita-core';
38
+
39
+ /** Marks a scroller adwaita-web has taken over, so the stylesheet can target it. */
40
+ const SHADED_CLASS = 'adw-scroll-shaded';
41
+
42
+ /** The custom property carrying the live overshoot size, in px. */
43
+ const OVERSHOOT_PROPERTY = '--adw-overshoot';
44
+
45
+ /** Which edge is currently overshooting, as a state class. */
46
+ const OVERSHOOT_CLASSES = ['overshoot-top', 'overshoot-bottom'] as const;
47
+
48
+ /**
49
+ * `overflow` values that make a box a scrollport.
50
+ *
51
+ * `overlay` is a WebKit alias for `auto` that a browser may still report from
52
+ * author CSS even though it is removed from the spec, so a scroller written that
53
+ * way is one and has to be treated as one.
54
+ */
55
+ const SCROLLABLE_OVERFLOW = new Set(['auto', 'scroll', 'overlay']);
56
+
57
+ /** Per-axis adjustments as the DOM reports them. */
58
+ function adjustmentsOf(el: HTMLElement): { vertical?: ScrollAdjustment; horizontal?: ScrollAdjustment } {
59
+ const style = getComputedStyle(el);
60
+ const vertical = SCROLLABLE_OVERFLOW.has(style.overflowY)
61
+ ? { value: el.scrollTop, lower: 0, upper: el.scrollHeight, pageSize: el.clientHeight }
62
+ : undefined;
63
+ const horizontal = SCROLLABLE_OVERFLOW.has(style.overflowX)
64
+ ? { value: el.scrollLeft, lower: 0, upper: el.scrollWidth, pageSize: el.clientWidth }
65
+ : undefined;
66
+ return { vertical, horizontal };
67
+ }
68
+
69
+ /** Whether a box scrolls on either axis at all. */
70
+ function isScroller(el: HTMLElement): boolean {
71
+ const style = getComputedStyle(el);
72
+ return SCROLLABLE_OVERFLOW.has(style.overflowY) || SCROLLABLE_OVERFLOW.has(style.overflowX);
73
+ }
74
+
75
+ /**
76
+ * A wheel delta in CSS pixels, whatever unit the event chose to speak in.
77
+ *
78
+ * `deltaMode` is not decoration — Firefox reports `DOM_DELTA_LINE` (3-ish per
79
+ * detent) where Chrome reports pixels (100-ish per detent), so an unconverted
80
+ * delta makes the overshoot two orders of magnitude smaller on one browser. The
81
+ * line height is the conventional 16px default scaled by the browser's own
82
+ * three-lines-per-detent step.
83
+ */
84
+ function wheelDeltaPx(event: WheelEvent, port: number): number {
85
+ if (event.deltaMode === WheelEvent.DOM_DELTA_LINE) return event.deltaY * 16;
86
+ if (event.deltaMode === WheelEvent.DOM_DELTA_PAGE) return event.deltaY * port;
87
+ return event.deltaY;
88
+ }
89
+
90
+ /** Which vertical edges may draw an undershoot — a toolbar view's own two flags. */
91
+ export interface AdwUndershootEdges {
92
+ /** `undershoot-top` on the view: the top bar is flat and was allocated a height. */
93
+ top: boolean;
94
+ /** `undershoot-bottom` on the view. */
95
+ bottom: boolean;
96
+ }
97
+
98
+ /**
99
+ * Keeps every scroller under a root marked with its live edge indicators.
100
+ *
101
+ * One instance per toolbar view. Scroll and wheel are listened for in the CAPTURE
102
+ * phase on the root: `scroll` does not bubble, but capture propagation runs down
103
+ * the whole path regardless, so one pair of listeners covers scrollers that appear
104
+ * later without re-binding per element.
105
+ */
106
+ export class AdwScrollShading {
107
+ private _root: HTMLElement;
108
+ private _mutations: MutationObserver | null = null;
109
+ private _resize: ResizeObserver | null = null;
110
+ /** The unclamped position per scroller, i.e. `priv->unclamped_vadj_value`. */
111
+ private _unclamped = new WeakMap<HTMLElement, number>();
112
+ private _settleTimers = new WeakMap<HTMLElement, ReturnType<typeof setTimeout>>();
113
+ /** Every scroller currently marked, so teardown and rescans can clear them. */
114
+ private _shaded = new Set<HTMLElement>();
115
+ private _pendingScan = 0;
116
+ private _undershootEdges: AdwUndershootEdges = { top: true, bottom: true };
117
+
118
+ constructor(root: HTMLElement) {
119
+ this._root = root;
120
+ }
121
+
122
+ /**
123
+ * Which edges may draw an UNDERSHOOT. The overshoot glow is not gated: upstream
124
+ * draws it on every `scrolledwindow`, while the undershoot is what a toolbar
125
+ * view turns on for a flat bar and off for a raised one, which has its own
126
+ * shadow (`update_undershoots`).
127
+ */
128
+ setUndershootEdges(edges: AdwUndershootEdges): void {
129
+ if (edges.top === this._undershootEdges.top && edges.bottom === this._undershootEdges.bottom) return;
130
+ this._undershootEdges = { ...edges };
131
+ for (const el of this._shaded) this._update(el);
132
+ }
133
+
134
+ /** Start observing. Idempotent. */
135
+ connect(): void {
136
+ if (this._mutations) return;
137
+ this._root.addEventListener('scroll', this._onScroll, { capture: true, passive: true });
138
+ this._root.addEventListener('wheel', this._onWheel, { capture: true, passive: true });
139
+ // A rescan reads the computed overflow of every descendant, so it is coalesced
140
+ // to one per frame: replacing a story fires a mutation record per node.
141
+ this._mutations = new MutationObserver(() => this._scheduleRefresh());
142
+ this._mutations.observe(this._root, { childList: true, subtree: true });
143
+ this._resize = new ResizeObserver(() => this._scheduleRefresh());
144
+ this._resize.observe(this._root);
145
+ this.refresh();
146
+ }
147
+
148
+ /** Stop observing and clear every class this instance set. */
149
+ disconnect(): void {
150
+ this._root.removeEventListener('scroll', this._onScroll, { capture: true });
151
+ this._root.removeEventListener('wheel', this._onWheel, { capture: true });
152
+ this._mutations?.disconnect();
153
+ this._mutations = null;
154
+ this._resize?.disconnect();
155
+ this._resize = null;
156
+ if (this._pendingScan) cancelAnimationFrame(this._pendingScan);
157
+ this._pendingScan = 0;
158
+ for (const el of this._shaded) this._clear(el);
159
+ this._shaded.clear();
160
+ }
161
+
162
+ private _scheduleRefresh(): void {
163
+ if (this._pendingScan) return;
164
+ this._pendingScan = requestAnimationFrame(() => {
165
+ this._pendingScan = 0;
166
+ this.refresh();
167
+ });
168
+ }
169
+
170
+ /** Re-scan for scrollers and re-derive every indicator. */
171
+ refresh(): void {
172
+ const found = new Set<HTMLElement>();
173
+ // The ROOT counts: a toolbar view whose content area scrolls on its own is
174
+ // the common case, and `querySelectorAll` never returns the element it was
175
+ // called on.
176
+ if (isScroller(this._root)) found.add(this._root);
177
+ for (const el of this._root.querySelectorAll<HTMLElement>('*')) {
178
+ if (isScroller(el)) found.add(el);
179
+ }
180
+ for (const el of this._shaded) {
181
+ if (!found.has(el)) this._clear(el);
182
+ }
183
+ this._shaded = found;
184
+ for (const el of found) this._update(el);
185
+ }
186
+
187
+ private _clear(el: HTMLElement): void {
188
+ el.classList.remove(SHADED_CLASS, ...ADW_UNDERSHOOT_CLASSES, ...OVERSHOOT_CLASSES);
189
+ el.style.removeProperty(OVERSHOOT_PROPERTY);
190
+ }
191
+
192
+ /** Re-derive the undershoot classes of one scroller from its current position. */
193
+ private _update(el: HTMLElement): void {
194
+ el.classList.add(SHADED_CLASS);
195
+ const live = scrollUndershootClasses(adjustmentsOf(el));
196
+ for (const cls of ADW_UNDERSHOOT_CLASSES) {
197
+ const allowed =
198
+ (cls !== 'undershoot-top' || this._undershootEdges.top) &&
199
+ (cls !== 'undershoot-bottom' || this._undershootEdges.bottom);
200
+ el.classList.toggle(cls, allowed && live.includes(cls));
201
+ }
202
+ }
203
+
204
+ private _onScroll = (event: Event): void => {
205
+ const el = event.target;
206
+ if (el instanceof HTMLElement && this._shaded.has(el)) this._update(el);
207
+ };
208
+
209
+ /**
210
+ * The overshoot half. A browser scroll container clamps at its edge and reports
211
+ * nothing further, so the push past it is read off the wheel deltas the
212
+ * container could not consume — which is exactly the value upstream keeps in
213
+ * `unclamped_vadj_value` and clamps one overshoot distance out.
214
+ */
215
+ private _onWheel = (event: WheelEvent): void => {
216
+ const el = event.target instanceof HTMLElement ? this._scrollerFor(event.target) : null;
217
+ if (!el) return;
218
+ const { vertical } = adjustmentsOf(el);
219
+ if (!vertical) return;
220
+
221
+ const previous = this._unclamped.get(el) ?? vertical.value;
222
+ // A scroll that landed back inside the range resets the accumulator: the
223
+ // position the user pushed from is the real one again.
224
+ const base = overshootDistance(previous, vertical) === 0 ? vertical.value : previous;
225
+ const next = accumulateOvershoot(base, wheelDeltaPx(event, vertical.pageSize), vertical);
226
+ this._unclamped.set(el, next);
227
+ this._applyOvershoot(el, overshootDistance(next, vertical));
228
+
229
+ // Spring back once the wheel STOPS, not between two detents of one gesture.
230
+ clearTimeout(this._settleTimers.get(el));
231
+ this._settleTimers.set(
232
+ el,
233
+ setTimeout(() => {
234
+ this._unclamped.set(el, el.scrollTop);
235
+ this._applyOvershoot(el, 0);
236
+ }, ADW_OVERSHOOT_SETTLE_MS),
237
+ );
238
+ };
239
+
240
+ private _applyOvershoot(el: HTMLElement, distance: number): void {
241
+ el.classList.toggle('overshoot-top', distance < 0);
242
+ el.classList.toggle('overshoot-bottom', distance > 0);
243
+ if (distance === 0) el.style.removeProperty(OVERSHOOT_PROPERTY);
244
+ else el.style.setProperty(OVERSHOOT_PROPERTY, `${Math.abs(distance)}px`);
245
+ }
246
+
247
+ /** The nearest shaded scroller at or above a wheel target, root included. */
248
+ private _scrollerFor(target: HTMLElement): HTMLElement | null {
249
+ for (let el: HTMLElement | null = target; el; el = el.parentElement) {
250
+ if (this._shaded.has(el)) return el;
251
+ if (el === this._root) break;
252
+ }
253
+ return null;
254
+ }
255
+ }
@@ -15,9 +15,21 @@ import { EditorView } from '@codemirror/view';
15
15
  import { HighlightStyle } from '@codemirror/language';
16
16
  import { tags } from '@lezer/highlight';
17
17
 
18
- /** Monospace stack — Adwaita Mono when present, else a portable code font. */
19
- const MONO_FONT =
20
- "'Adwaita Mono', 'Cascadia Code', 'JetBrains Mono', 'Source Code Pro', ui-monospace, 'SF Mono', Menlo, Consolas, monospace";
18
+ /**
19
+ * Monospace stack — the `--monospace-font-family` token, with its own value
20
+ * repeated as the `var()` fallback for a page that loads `@gjsify/adwaita-web`'s
21
+ * stylesheet without the theme tokens.
22
+ *
23
+ * It USED to be a second stack, written out here and longer (Cascadia Code,
24
+ * JetBrains Mono, Source Code Pro, Consolas). Two consequences, both real: the
25
+ * `stylesheet-font-families` conformance rule reads `.css`/`.scss` only, so a
26
+ * stack living in a TS string literal was outside every check the repo has; and
27
+ * on a host with, say, JetBrains Mono but no Adwaita Mono, an `<adw-source-view>`
28
+ * and a `.monospace` label on the SAME page rendered in DIFFERENT typefaces.
29
+ * `_variables.scss` is where that stack is argued about (it explains why
30
+ * 'Cantarell' is dropped from it), so it is where it lives.
31
+ */
32
+ const MONO_FONT = "var(--monospace-font-family, 'Adwaita Mono', ui-monospace, 'SF Mono', 'Menlo', monospace)";
21
33
 
22
34
  /** The CodeMirror EditorView theme mapping `.cm-*` chrome to Adwaita tokens. */
23
35
  export const adwaitaEditorTheme = EditorView.theme({
@@ -19,6 +19,7 @@ import {
19
19
  } from '@gjsify/adwaita-core/conformance';
20
20
  import type { SplitButtonProperty } from '@gjsify/adwaita-core';
21
21
  import type { AdwSplitButton } from './elements/adw-split-button.js';
22
+ import { fallbackMask, maskOf } from './icon-registry.spec.js';
22
23
 
23
24
  /** The GObject properties the element re-emits as `notify::*` CustomEvents. */
24
25
  const NOTIFY_PROPERTIES: readonly SplitButtonProperty[] = [
@@ -160,12 +161,15 @@ export const AdwSplitButtonTest = async () => {
160
161
  host.remove();
161
162
  });
162
163
 
163
- await it('keeps a stray icon name out of the class list', () => {
164
+ await it('turns a stray icon name into the broken glyph, not a stray class', () => {
164
165
  const { el, host } = mount({ 'icon-name': 'a b' });
165
166
  const icon = actionHalf(el).querySelector('.adw-icon') as HTMLElement;
166
167
  // `adw-icon--a b` would have added a second, unrelated `b` class.
167
168
  expect(icon.classList.contains('b')).toBe(false);
168
- expect(icon.className).toBe('adw-icon');
169
+ expect(icon.className).toBe('adw-icon adw-icon--image-missing');
170
+ // The name was GIVEN, so the slot shows what GTK's failed lookup shows —
171
+ // dropping the class left a visible 16px hole instead.
172
+ expect(maskOf(icon)).toBe(fallbackMask());
169
173
  host.remove();
170
174
  });
171
175
 
@@ -83,6 +83,12 @@ export const DOCUMENTED_STYLE_CLASSES: Record<string, { shipped: boolean; reason
83
83
  monospace: SHIPPED,
84
84
  numeric: SHIPPED,
85
85
  // Colors
86
+ // `accent` was missing from this list for as long as it has existed, and not by
87
+ // judgement: the gate that keeps the list honest read only the doc's BACKTICK
88
+ // spelling, and `.accent` is the one class upstream writes as `<tt>.accent</tt>`
89
+ // (style-classes.md:368). It has always been implemented (`scss/_labels.scss:53`);
90
+ // what was missing was anything that would notice if it stopped being.
91
+ accent: SHIPPED,
86
92
  error: SHIPPED,
87
93
  warning: SHIPPED,
88
94
  success: SHIPPED,