@gjsify/adwaita-web 0.31.0 → 0.32.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 (176) hide show
  1. package/dist/adwaita-web.css +786 -272
  2. package/dist/adwaita-web.css.map +1 -1
  3. package/lib/types/adw-about-dialog.spec.d.ts +1 -0
  4. package/lib/types/adw-action-rows.spec.d.ts +1 -0
  5. package/lib/types/adw-avatar.spec.d.ts +1 -0
  6. package/lib/types/adw-banner.spec.d.ts +1 -0
  7. package/lib/types/adw-button-content.spec.d.ts +1 -0
  8. package/lib/types/adw-carousel.spec.d.ts +1 -0
  9. package/lib/types/adw-checks.spec.d.ts +1 -0
  10. package/lib/types/adw-header-bar.spec.d.ts +1 -0
  11. package/lib/types/adw-icon.spec.d.ts +1 -0
  12. package/lib/types/adw-progress-bar.spec.d.ts +1 -0
  13. package/lib/types/adw-sidebar.spec.d.ts +1 -0
  14. package/lib/types/adw-switch.spec.d.ts +1 -0
  15. package/lib/types/adw-wrap-box.spec.d.ts +1 -0
  16. package/lib/types/bottom-sheet.spec.d.ts +1 -0
  17. package/lib/types/breakpoints.d.ts +22 -0
  18. package/lib/types/breakpoints.spec.d.ts +1 -0
  19. package/lib/types/chrome.spec.d.ts +1 -0
  20. package/lib/types/elements/adw-about-dialog.d.ts +47 -8
  21. package/lib/types/elements/adw-action-row.d.ts +35 -0
  22. package/lib/types/elements/adw-avatar.d.ts +11 -0
  23. package/lib/types/elements/adw-bottom-sheet.d.ts +33 -4
  24. package/lib/types/elements/adw-button-content.d.ts +12 -0
  25. package/lib/types/elements/adw-button-row.d.ts +10 -1
  26. package/lib/types/elements/adw-carousel.d.ts +111 -8
  27. package/lib/types/elements/adw-checks.d.ts +57 -0
  28. package/lib/types/elements/adw-clamp.d.ts +4 -1
  29. package/lib/types/elements/adw-data-grid.d.ts +11 -31
  30. package/lib/types/elements/adw-drop-down.d.ts +17 -20
  31. package/lib/types/elements/adw-entry-row.d.ts +93 -3
  32. package/lib/types/elements/adw-expander-row.d.ts +7 -1
  33. package/lib/types/elements/adw-header-bar.d.ts +15 -0
  34. package/lib/types/elements/adw-icon.d.ts +25 -0
  35. package/lib/types/elements/adw-inline-view-switcher.d.ts +37 -11
  36. package/lib/types/elements/adw-menu-button.d.ts +6 -6
  37. package/lib/types/elements/adw-navigation-split-view.d.ts +6 -0
  38. package/lib/types/elements/adw-navigation-view.d.ts +63 -20
  39. package/lib/types/elements/adw-overlay-split-view.d.ts +38 -0
  40. package/lib/types/elements/adw-password-entry-row.d.ts +19 -10
  41. package/lib/types/elements/adw-popover.d.ts +58 -0
  42. package/lib/types/elements/adw-preferences-dialog.d.ts +57 -2
  43. package/lib/types/elements/adw-preferences-group.d.ts +31 -0
  44. package/lib/types/elements/adw-progress-bar.d.ts +38 -0
  45. package/lib/types/elements/adw-sidebar.d.ts +37 -5
  46. package/lib/types/elements/adw-split-button.d.ts +53 -7
  47. package/lib/types/elements/adw-switch-row.d.ts +11 -2
  48. package/lib/types/elements/adw-switch.d.ts +15 -0
  49. package/lib/types/elements/adw-tab-view.d.ts +129 -12
  50. package/lib/types/elements/adw-toolbar-view.d.ts +5 -0
  51. package/lib/types/elements/adw-view-stack.d.ts +27 -19
  52. package/lib/types/elements/adw-view-switcher-bar.d.ts +23 -7
  53. package/lib/types/elements/adw-view-switcher.d.ts +22 -11
  54. package/lib/types/elements/adw-window-title.d.ts +3 -1
  55. package/lib/types/elements/view-switcher-dom.d.ts +44 -0
  56. package/lib/types/entry-rows.spec.d.ts +1 -0
  57. package/lib/types/index.d.ts +7 -0
  58. package/lib/types/navigation-view.spec.d.ts +1 -0
  59. package/lib/types/preferences-search.d.ts +20 -0
  60. package/lib/types/preferences.spec.d.ts +1 -0
  61. package/lib/types/split-button.spec.d.ts +1 -0
  62. package/lib/types/split-views.spec.d.ts +1 -0
  63. package/lib/types/styles.generated.d.ts +1 -1
  64. package/lib/types/tab-view.spec.d.ts +1 -0
  65. package/lib/types/view-stack.spec.d.ts +1 -0
  66. package/lib/types/view-switcher.spec.d.ts +1 -0
  67. package/package.json +6 -6
  68. package/scss/_banner.scss +13 -0
  69. package/scss/_bottom_sheet.scss +5 -6
  70. package/scss/_button.scss +3 -26
  71. package/scss/_button_content.scss +28 -0
  72. package/scss/_checks.scss +228 -0
  73. package/scss/_clamp.scss +9 -3
  74. package/scss/_data_grid.scss +15 -7
  75. package/scss/_drop_down.scss +12 -55
  76. package/scss/_expander_row.scss +5 -50
  77. package/scss/_icon.scss +51 -0
  78. package/scss/_icons.generated.scss +6 -0
  79. package/scss/_inline_view_switcher.scss +34 -0
  80. package/scss/_labels.scss +135 -0
  81. package/scss/_menu_button.scss +10 -70
  82. package/scss/_popover.scss +186 -0
  83. package/scss/_preferences.scss +29 -0
  84. package/scss/_preferences_dialog.scss +17 -0
  85. package/scss/_progress_bar.scss +161 -0
  86. package/scss/_reset.scss +18 -7
  87. package/scss/_spinner.scss +7 -3
  88. package/scss/_split_button.scss +6 -50
  89. package/scss/_switch.scss +90 -0
  90. package/scss/_switch_row.scss +6 -52
  91. package/scss/_tab_view.scss +64 -0
  92. package/scss/_theme.scss +10 -0
  93. package/scss/_toolbar_view.scss +74 -0
  94. package/scss/_variables.scss +34 -0
  95. package/scss/_view_switcher.scss +40 -0
  96. package/scss/_view_switcher_bar.scss +37 -0
  97. package/scss/_wrap_box.scss +56 -6
  98. package/scss/adwaita-skin.scss +23 -0
  99. package/src/adw-about-dialog.spec.ts +350 -0
  100. package/src/adw-action-rows.spec.ts +342 -0
  101. package/src/adw-avatar.spec.ts +139 -0
  102. package/src/adw-banner.spec.ts +152 -0
  103. package/src/adw-button-content.spec.ts +183 -0
  104. package/src/adw-button-row.spec.ts +12 -6
  105. package/src/adw-carousel.spec.ts +389 -0
  106. package/src/adw-checks.spec.ts +368 -0
  107. package/src/adw-drop-down.spec.ts +123 -0
  108. package/src/adw-header-bar.spec.ts +83 -0
  109. package/src/adw-icon.spec.ts +167 -0
  110. package/src/adw-progress-bar.spec.ts +173 -0
  111. package/src/adw-sidebar.spec.ts +332 -0
  112. package/src/adw-switch.spec.ts +242 -0
  113. package/src/adw-tab-view.spec.ts +12 -3
  114. package/src/adw-view-switcher-bar.spec.ts +61 -13
  115. package/src/adw-wrap-box.spec.ts +194 -0
  116. package/src/bottom-sheet.spec.ts +325 -0
  117. package/src/breakpoints.spec.ts +147 -0
  118. package/src/breakpoints.ts +67 -0
  119. package/src/chrome.spec.ts +301 -0
  120. package/src/elements/adw-about-dialog.ts +223 -163
  121. package/src/elements/adw-action-row.ts +120 -10
  122. package/src/elements/adw-avatar.ts +48 -54
  123. package/src/elements/adw-banner.ts +46 -10
  124. package/src/elements/adw-bottom-sheet.ts +145 -60
  125. package/src/elements/adw-button-content.ts +107 -33
  126. package/src/elements/adw-button-row.ts +61 -37
  127. package/src/elements/adw-button.ts +5 -7
  128. package/src/elements/adw-carousel.ts +467 -105
  129. package/src/elements/adw-checks.ts +299 -0
  130. package/src/elements/adw-clamp.ts +80 -10
  131. package/src/elements/adw-combo-row.ts +15 -7
  132. package/src/elements/adw-data-grid.ts +43 -91
  133. package/src/elements/adw-drop-down.ts +117 -128
  134. package/src/elements/adw-entry-row.ts +404 -36
  135. package/src/elements/adw-expander-row.ts +61 -29
  136. package/src/elements/adw-header-bar.ts +31 -6
  137. package/src/elements/adw-icon.ts +148 -0
  138. package/src/elements/adw-inline-view-switcher.ts +240 -129
  139. package/src/elements/adw-menu-button.ts +93 -70
  140. package/src/elements/adw-navigation-split-view.ts +48 -7
  141. package/src/elements/adw-navigation-view.ts +286 -148
  142. package/src/elements/adw-overlay-split-view.ts +164 -14
  143. package/src/elements/adw-password-entry-row.ts +111 -89
  144. package/src/elements/adw-popover.ts +283 -0
  145. package/src/elements/adw-preferences-dialog.ts +120 -21
  146. package/src/elements/adw-preferences-group.ts +128 -22
  147. package/src/elements/adw-progress-bar.ts +189 -0
  148. package/src/elements/adw-sidebar.ts +301 -142
  149. package/src/elements/adw-spin-row.ts +11 -5
  150. package/src/elements/adw-spinner.ts +23 -8
  151. package/src/elements/adw-split-button.ts +371 -92
  152. package/src/elements/adw-status-page.ts +9 -9
  153. package/src/elements/adw-switch-row.ts +100 -30
  154. package/src/elements/adw-switch.ts +129 -0
  155. package/src/elements/adw-tab-view.ts +682 -129
  156. package/src/elements/adw-toggle-group.ts +5 -7
  157. package/src/elements/adw-toolbar-view.ts +84 -21
  158. package/src/elements/adw-view-stack.ts +162 -85
  159. package/src/elements/adw-view-switcher-bar.ts +163 -81
  160. package/src/elements/adw-view-switcher.ts +231 -112
  161. package/src/elements/adw-window-title.ts +44 -11
  162. package/src/elements/adw-wrap-box.ts +131 -35
  163. package/src/elements/view-switcher-dom.ts +108 -0
  164. package/src/entry-rows.spec.ts +391 -0
  165. package/src/index.ts +23 -0
  166. package/src/navigation-view.spec.ts +435 -0
  167. package/src/preferences-search.ts +135 -0
  168. package/src/preferences.spec.ts +250 -0
  169. package/src/source-view/adw-source-view.ts +2 -5
  170. package/src/split-button.spec.ts +470 -0
  171. package/src/split-views.spec.ts +309 -0
  172. package/src/styles.generated.ts +1 -1
  173. package/src/tab-view.spec.ts +451 -0
  174. package/src/test.browser.mts +49 -0
  175. package/src/view-stack.spec.ts +272 -0
  176. package/src/view-switcher.spec.ts +446 -0
@@ -1,25 +1,39 @@
1
1
  // <adw-about-dialog> — The standard Adwaita "About" dialog (the web counterpart
2
2
  // of Adw.AboutDialog). It is a modal AdwDialog (a full-cover scrim + a centred,
3
3
  // scrollable floating sheet) hosting a navigation view: the main page shows the
4
- // app icon, name, developer name and a version pill, with preference rows that
5
- // navigate to the Details, Credits and Legal subpages.
4
+ // app icon, name, developer name and a version pill, followed by up to three
5
+ // preference groups of rows that link out or push a subpage.
6
6
  //
7
7
  // Like Adw.AboutDialog the dialog is presented imperatively — `present()` (or
8
8
  // setting the `open` attribute) reveals it; Escape, a scrim click, the header
9
9
  // close button, or `close()` dismisses it. The header title fades in once the
10
10
  // main page is scrolled (mirroring AdwAboutDialog's update_headerbar_cb).
11
11
  //
12
+ // WHICH PAGE A ROW IS ON IS NOT A LAYOUT CHOICE — it is `update_details` and
13
+ // `update_support`, and this element used to get it wrong in three ways: it put
14
+ // Support Questions and Report an Issue on the DETAILS page, where the template
15
+ // keeps them on the MAIN page (adw-about-dialog.ui:146-186); it treated
16
+ // `website` as Details content, which is the one input libadwaita deliberately
17
+ // leaves out of that predicate (adw-about-dialog.c:1108), so a dialog whose only
18
+ // extra was a website grew a Details subpage holding a single link; and it
19
+ // invented fallbacks GTK does not have — "Application" for a missing name, a
20
+ // generic executable glyph for a missing icon, and an "About <app>" title on top
21
+ // of an "About <app>" label. All four derivations now come from
22
+ // `@gjsify/adwaita-core`, which holds them once for both renderers (ADR 0004)
23
+ // and is held to `@gjsify/adwaita-core/conformance`.
24
+ //
12
25
  // Properties / attributes (all reflected, mirroring the AdwAboutDialog GObject
13
26
  // properties of the same name, hyphenated):
14
27
  // application-name the application's display name (shown as a title-1)
15
28
  // application-icon a symbolic icon name (with or without -symbolic) — the
16
- // large app icon; falls back to a generic glyph
29
+ // large app icon; absent one, no icon is drawn
17
30
  // developer-name the developer / vendor line below the name
18
31
  // version the version, shown as a clickable pill
19
32
  // comments a longer description (shown on the Details page)
20
- // website the application's website URL (a Website link row)
21
- // support-url a support-forum URL (a Support Questions link row)
22
- // issue-url an issue-tracker URL (a Report an Issue link row)
33
+ // website the application's website URL — a Website link row, on
34
+ // the MAIN page unless the Details page has other content
35
+ // support-url a support-forum URL (a main-page Support Questions row)
36
+ // issue-url an issue-tracker URL (a main-page Report an Issue row)
23
37
  // copyright the copyright line (shown on the Legal page)
24
38
  // license the license text/name (shown on the Legal page); when a
25
39
  // `license-url` is also given the name becomes a link
@@ -29,14 +43,23 @@
29
43
  // List-valued credits are set via properties (string arrays), mirroring the
30
44
  // AdwAboutDialog `developers` / `designers` / `artists` / `documenters`
31
45
  // properties — entries may be `Name`, `Name <email>` or `Name https://url`,
32
- // matching the AdwAboutDialog credit syntax.
46
+ // parsed by the core's `parseCreditPerson`, which is `parse_person` (:490-534).
47
+ //
48
+ // NOT implemented here (and therefore left at their empty values when the core
49
+ // derivations are asked): `release-notes` + the What's New page, `debug-info` +
50
+ // the Troubleshooting page, `add_link`, `add_credit_section`,
51
+ // `translator-credits` and the Acknowledgements page. The core covers all of
52
+ // them; adopting them is a feature, not a fidelity fix.
33
53
  //
34
54
  // Events (CustomEvent, bubbles):
35
55
  // `notify::open` (detail = { open }) when the presented state changes —
36
56
  // mirrors the Adw.Dialog `open` notification.
37
57
  // `closed` when the dialog is dismissed — mirrors Adw.Dialog::closed.
38
- // `activate-link` (detail = { uri }) when a link row is activated — mirrors
39
- // AdwAboutDialog::activate-link (not cancelled here; the browser navigates).
58
+ // `activate-link` (detail = { uri }, CANCELABLE) when a link is activated —
59
+ // mirrors AdwAboutDialog::activate-link, a boolean signal with the
60
+ // `true_handled` accumulator (:2093-2103): a handler that returns TRUE
61
+ // suppresses the default navigation (:429-437). `preventDefault()` is the
62
+ // web spelling of returning TRUE.
40
63
  //
41
64
  // Reference: refs/libadwaita/src/adw-about-dialog.c (AdwAboutDialog behaviour)
42
65
  // Reference: refs/libadwaita/src/adw-about-dialog.ui (navigation page tree)
@@ -44,30 +67,24 @@
44
67
  // Reference: refs/libadwaita/src/stylesheet/widgets/_dialogs.scss (floating sheet)
45
68
  // Reference: refs/adwaita-web/adwaita-web/scss/_about_dialog.scss (web layout)
46
69
  // Copyright (c) 2022-2024 GNOME Foundation Inc. / Purism SPC (libadwaita). LGPLv2.1+.
47
- // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
48
-
49
- /** A credit person — a plain name, or a name paired with a link (email / URL). */
50
- interface CreditEntry {
51
- name: string;
52
- uri: string | null;
53
- }
54
-
55
- /** Parse an AdwAboutDialog credit string: `Name`, `Name <email>`, `Name https://…`. */
56
- function parseCredit(raw: string): CreditEntry {
57
- const trimmed = raw.trim();
58
- // `Name <email-or-url>` — the bracketed part is the link target.
59
- const angle = /^(.*?)\s*<([^>]+)>$/.exec(trimmed);
60
- if (angle) {
61
- const target = angle[2].trim();
62
- const uri = target.includes('@') && !/^[a-z][a-z0-9+.-]*:/i.test(target) ? `mailto:${target}` : target;
63
- return { name: angle[1].trim(), uri };
64
- }
65
- // `Name https://url` — a trailing bare URL is the link target.
66
- const url = /^(.*?)\s+((?:https?|mailto):\S+)$/.exec(trimmed);
67
- if (url) {
68
- return { name: url[1].trim(), uri: url[2].trim() };
69
- }
70
- return { name: trimmed, uri: null };
70
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web; the
71
+ // icon nodes are <adw-icon>; the derivations live in @gjsify/adwaita-core.
72
+
73
+ import {
74
+ ADW_ABOUT_DIALOG_LABELS,
75
+ type AdwCreditPerson,
76
+ GTK_LICENSE,
77
+ aboutDialogVisibility,
78
+ creditsSections,
79
+ legalSectionVisible,
80
+ stripMnemonic,
81
+ } from '@gjsify/adwaita-core';
82
+
83
+ import { createAdwIcon } from './adw-icon.js';
84
+
85
+ /** A template label with its mnemonic marker removed — this renderer has no accelerator layer. */
86
+ function rowLabel(key: keyof typeof ADW_ABOUT_DIALOG_LABELS): string {
87
+ return stripMnemonic(ADW_ABOUT_DIALOG_LABELS[key]);
71
88
  }
72
89
 
73
90
  export class AdwAboutDialog extends HTMLElement {
@@ -265,6 +282,10 @@ export class AdwAboutDialog extends HTMLElement {
265
282
  this._sheetEl.className = 'adw-about-dialog-sheet';
266
283
  this._sheetEl.setAttribute('role', 'dialog');
267
284
  this._sheetEl.setAttribute('aria-modal', 'true');
285
+ // AdwDialog:title, whose template default is the bare word (.ui:6). The
286
+ // application name is announced by the title-1 label inside, exactly as
287
+ // the header revealer shows it separately in GTK (.ui:29-31).
288
+ this._sheetEl.setAttribute('aria-label', ADW_ABOUT_DIALOG_LABELS.dialogTitle);
268
289
 
269
290
  this.replaceChildren(this._scrimEl, this._sheetEl);
270
291
 
@@ -305,23 +326,56 @@ export class AdwAboutDialog extends HTMLElement {
305
326
  }
306
327
  }
307
328
 
329
+ /** The credit groups the Credits page shows, straight from `update_credits`. */
330
+ private _creditsSections() {
331
+ return creditsSections({
332
+ developers: this._developers,
333
+ designers: this._designers,
334
+ artists: this._artists,
335
+ documenters: this._documenters,
336
+ });
337
+ }
338
+
339
+ /**
340
+ * Whether the Legal page has anything on it.
341
+ *
342
+ * This element's licence model is the simplified one — a name plus an
343
+ * optional URL — so it maps onto `GtkLicense` as "custom text when a licence
344
+ * is set, unknown otherwise", which is what `adw_about_dialog_set_license`
345
+ * does to the type anyway (:3472).
346
+ */
347
+ private _hasLegal(): boolean {
348
+ const licenseType = this.license.length > 0 ? GTK_LICENSE.CUSTOM : GTK_LICENSE.UNKNOWN;
349
+ return legalSectionVisible(this.copyright, licenseType, this.license);
350
+ }
351
+
308
352
  /** Rebuild the sheet contents from the current attributes / credit lists. */
309
353
  private _render(): void {
310
- const appName = this.applicationName || 'Application';
311
- this._sheetEl.setAttribute('aria-label', `About ${appName}`);
354
+ const sections = this._creditsSections();
355
+ // The four inputs this element has no API for stay empty: `debug-info`,
356
+ // `release-notes`, `add_link` and the acknowledgements page.
357
+ const visibility = aboutDialogVisibility({
358
+ applicationIcon: this.applicationIcon,
359
+ applicationName: this.applicationName,
360
+ developerName: this.developerName,
361
+ version: this.version,
362
+ comments: this.comments,
363
+ website: this.website,
364
+ supportUrl: this.supportUrl,
365
+ issueUrl: this.issueUrl,
366
+ hasCredits: sections.length > 0,
367
+ hasLegal: this._hasLegal(),
368
+ });
312
369
 
313
370
  // The navigation view (main page + subpages) lives inside the sheet.
314
371
  this._navView = document.createElement('adw-navigation-view');
315
- this._navView.append(this._buildMainPage(appName));
316
-
317
- const detailsPage = this._buildDetailsPage();
318
- if (detailsPage) this._navView.append(detailsPage);
319
-
320
- const creditsPage = this._buildCreditsPage();
321
- if (creditsPage) this._navView.append(creditsPage);
372
+ this._navView.append(this._buildMainPage(visibility));
322
373
 
323
- const legalPage = this._buildLegalPage();
324
- if (legalPage) this._navView.append(legalPage);
374
+ // The Details page exists exactly when its row does — `details_row` is
375
+ // `has_comments || show_links` (:1115), i.e. "the page has content".
376
+ if (visibility.detailsRow) this._navView.append(this._buildDetailsPage(visibility));
377
+ if (visibility.creditsRow) this._navView.append(this._buildCreditsPage(sections));
378
+ if (visibility.legalRow) this._navView.append(this._buildLegalPage());
325
379
 
326
380
  this._sheetEl.replaceChildren(this._navView);
327
381
  this._sheetEl.tabIndex = -1;
@@ -329,11 +383,18 @@ export class AdwAboutDialog extends HTMLElement {
329
383
  this.classList.toggle('open', this.open);
330
384
  }
331
385
 
332
- /** A navigation page wrapping a header bar (with the close button) + content. */
386
+ /**
387
+ * A navigation page wrapping a header bar (with the close button) + content.
388
+ *
389
+ * `headerTitle` defaults to the page title; the main page overrides it,
390
+ * because its page title is the dialog's ("About", .ui:19) while its header
391
+ * shows the application name (.ui:30).
392
+ */
333
393
  private _buildPage(
334
394
  tag: string,
335
395
  title: string,
336
396
  showClose: boolean,
397
+ headerTitle: string = title,
337
398
  ): {
338
399
  page: HTMLElement;
339
400
  body: HTMLDivElement;
@@ -346,7 +407,7 @@ export class AdwAboutDialog extends HTMLElement {
346
407
  const header = document.createElement('adw-header-bar');
347
408
  // The main page's title is hidden until the page is scrolled (CSS), the
348
409
  // subpages always show it (AdwAboutDialog update_headerbar_cb).
349
- header.setAttribute('title', title);
410
+ header.setAttribute('title', headerTitle);
350
411
 
351
412
  if (showClose) {
352
413
  // The close button — drawn with a CSS "×" glyph (no window-close
@@ -392,27 +453,30 @@ export class AdwAboutDialog extends HTMLElement {
392
453
  return { page, body, header };
393
454
  }
394
455
 
395
- /** The main page: icon, name, developer, version pill, navigation rows. */
396
- private _buildMainPage(appName: string): HTMLElement {
397
- const { page, body } = this._buildPage('main', `About ${appName}`, true);
456
+ /** The main page: icon, name, developer, version pill, then the three groups. */
457
+ private _buildMainPage(visibility: ReturnType<typeof aboutDialogVisibility>): HTMLElement {
458
+ const { page, body } = this._buildPage('main', ADW_ABOUT_DIALOG_LABELS.dialogTitle, true, this.applicationName);
398
459
 
399
- // App icon — a large symbolic glyph. A given application-icon resolves
400
- // to its `.adw-icon--<name>` mask class; absent one, fall back to the
401
- // GTK default generic `application-x-executable` glyph.
402
- const icon = (this.applicationIcon || 'application-x-executable').replace(/-symbolic$/, '');
403
- const iconEl = document.createElement('span');
404
- iconEl.className = `adw-about-dialog-icon adw-icon adw-icon--${icon}`;
405
- iconEl.setAttribute('aria-hidden', 'true');
406
- body.appendChild(iconEl);
460
+ // App icon — a large symbolic glyph, and only when one was given. GTK
461
+ // hides the image outright for an empty icon name (:2313-2314); picking
462
+ // a generic glyph here would show an icon for an application that asked
463
+ // for none. A reverse-DNS application id (`org.gnome.Builder`) is not one
464
+ // CSS token and resolves to no glyph rather than to three stray classes —
465
+ // see `normalizeIconName`.
466
+ if (visibility.appIcon) {
467
+ body.appendChild(createAdwIcon(this.applicationIcon, 'adw-about-dialog-icon'));
468
+ }
407
469
 
408
- // App name — title-1.
409
- const nameEl = document.createElement('span');
410
- nameEl.className = 'adw-about-dialog-name';
411
- nameEl.textContent = appName;
412
- body.appendChild(nameEl);
470
+ // App name — title-1. No fallback: an unset name means no label (:2376-2377).
471
+ if (visibility.appName) {
472
+ const nameEl = document.createElement('span');
473
+ nameEl.className = 'adw-about-dialog-name';
474
+ nameEl.textContent = this.applicationName;
475
+ body.appendChild(nameEl);
476
+ }
413
477
 
414
478
  // Developer / vendor.
415
- if (this.developerName) {
479
+ if (visibility.developerName) {
416
480
  const devEl = document.createElement('span');
417
481
  devEl.className = 'adw-about-dialog-developer';
418
482
  devEl.textContent = this.developerName;
@@ -420,52 +484,72 @@ export class AdwAboutDialog extends HTMLElement {
420
484
  }
421
485
 
422
486
  // Version pill.
423
- if (this.version) {
487
+ if (visibility.version) {
424
488
  const versionEl = document.createElement('span');
425
489
  versionEl.className = 'adw-about-dialog-version';
426
490
  versionEl.textContent = this.version;
427
491
  body.appendChild(versionEl);
428
492
  }
429
493
 
430
- // The navigation preference rows (Details / Credits / Legal). Only the
431
- // rows whose corresponding subpage has content are shown — mirroring the
432
- // AdwAboutDialog group visibility bindings.
433
- const group = document.createElement('adw-preferences-group');
434
- let hasRows = false;
435
-
436
- if (this._hasDetails()) {
437
- group.appendChild(this._buildNavRow('Details', 'details'));
438
- hasRows = true;
494
+ // Three separate preference groups, as in the template: details_group
495
+ // (.ui:96), support_group (.ui:146) and credits_legal_group (.ui:188).
496
+ // They are groups, not one list, and the stylesheet already spaces
497
+ // consecutive `.adw-about-dialog-rows` apart for exactly this.
498
+ const detailsGroupRows: HTMLElement[] = [];
499
+ if (visibility.detailsRow) {
500
+ detailsGroupRows.push(this._buildNavRow(rowLabel('detailsRow'), 'details'));
439
501
  }
440
- if (this._hasCredits()) {
441
- group.appendChild(this._buildNavRow('Credits', 'credits'));
442
- hasRows = true;
443
- }
444
- if (this._hasLegal()) {
445
- group.appendChild(this._buildNavRow('Legal', 'legal'));
446
- hasRows = true;
502
+ if (visibility.websiteRow) {
503
+ detailsGroupRows.push(this._buildLinkRow(rowLabel('websiteRow'), this.website));
447
504
  }
448
505
 
449
- if (hasRows) {
450
- const groupWrap = document.createElement('div');
451
- groupWrap.className = 'adw-about-dialog-rows';
452
- groupWrap.appendChild(group);
453
- body.appendChild(groupWrap);
506
+ const supportGroupRows: HTMLElement[] = [];
507
+ if (visibility.supportRow) {
508
+ supportGroupRows.push(this._buildLinkRow(rowLabel('supportRow'), this.supportUrl));
509
+ }
510
+ if (visibility.issueRow) {
511
+ supportGroupRows.push(this._buildLinkRow(rowLabel('issueRow'), this.issueUrl));
454
512
  }
455
513
 
514
+ const creditsLegalRows: HTMLElement[] = [];
515
+ if (visibility.creditsRow) creditsLegalRows.push(this._buildNavRow(rowLabel('creditsRow'), 'credits'));
516
+ if (visibility.legalRow) creditsLegalRows.push(this._buildNavRow(rowLabel('legalRow'), 'legal'));
517
+
518
+ if (visibility.detailsGroup) body.appendChild(this._buildGroup('details', detailsGroupRows));
519
+ if (visibility.supportGroup) body.appendChild(this._buildGroup('support', supportGroupRows));
520
+ if (visibility.creditsLegalGroup) body.appendChild(this._buildGroup('credits-legal', creditsLegalRows));
521
+
456
522
  return page;
457
523
  }
458
524
 
525
+ /**
526
+ * One `AdwPreferencesGroup` in its spacing wrapper.
527
+ *
528
+ * `data-group` carries the template's widget id so a test — and a reader —
529
+ * can tell the four named groups apart; nothing styles off it. The credits
530
+ * sections pass `null`, because the template has no id for them: they are
531
+ * built one per section at runtime (:548-549).
532
+ */
533
+ private _buildGroup(id: string | null, rows: HTMLElement[], title?: string): HTMLElement {
534
+ const group = document.createElement('adw-preferences-group');
535
+ if (id !== null) group.setAttribute('data-group', id);
536
+ if (title !== undefined) group.setAttribute('title', title);
537
+ for (const row of rows) group.appendChild(row);
538
+
539
+ const wrap = document.createElement('div');
540
+ wrap.className = 'adw-about-dialog-rows';
541
+ wrap.appendChild(group);
542
+ return wrap;
543
+ }
544
+
459
545
  /** An activatable row that pushes a subpage, with a trailing go-next chevron. */
460
546
  private _buildNavRow(title: string, tag: string): HTMLElement {
461
547
  const row = document.createElement('adw-action-row');
462
548
  row.setAttribute('title', title);
463
549
  row.setAttribute('activatable', '');
464
550
 
465
- const chevron = document.createElement('span');
551
+ const chevron = createAdwIcon('go-next', 'adw-about-dialog-chevron');
466
552
  chevron.setAttribute('slot', 'suffix');
467
- chevron.className = 'adw-icon adw-icon--go-next adw-about-dialog-chevron';
468
- chevron.setAttribute('aria-hidden', 'true');
469
553
  row.appendChild(chevron);
470
554
 
471
555
  row.addEventListener('activated', () => {
@@ -476,47 +560,37 @@ export class AdwAboutDialog extends HTMLElement {
476
560
  return row;
477
561
  }
478
562
 
479
- private _hasDetails(): boolean {
480
- return Boolean(this.comments || this.website || this.supportUrl || this.issueUrl);
481
- }
482
-
483
- private _hasCredits(): boolean {
484
- return (
485
- this._developers.length > 0 ||
486
- this._designers.length > 0 ||
487
- this._artists.length > 0 ||
488
- this._documenters.length > 0
489
- );
563
+ /**
564
+ * Emit `activate-link` and navigate unless a handler took over.
565
+ *
566
+ * The GObject signal is `BOOLEAN__STRING` with `g_signal_accumulator_true_handled`
567
+ * (:2093-2103) and its default handler returns `GDK_EVENT_PROPAGATE`
568
+ * (:455-460), i.e. "not handled, do the default". A handler returning TRUE
569
+ * suppresses it — which is what `preventDefault()` on a cancelable event
570
+ * means, and why `dispatchEvent`'s return value is the gate here.
571
+ */
572
+ private _activateLink(uri: string): void {
573
+ const event = new CustomEvent('activate-link', { bubbles: true, cancelable: true, detail: { uri } });
574
+ if (this.dispatchEvent(event)) window.open(uri, '_blank', 'noopener,noreferrer');
490
575
  }
491
576
 
492
- private _hasLegal(): boolean {
493
- return Boolean(this.copyright || this.license);
494
- }
577
+ /** The Details page: comments + the links group (website + future custom links). */
578
+ private _buildDetailsPage(visibility: ReturnType<typeof aboutDialogVisibility>): HTMLElement {
579
+ const { page, body } = this._buildPage('details', ADW_ABOUT_DIALOG_LABELS.detailsPage, false);
495
580
 
496
- /** The Details page: comments + website / support / issue link rows. */
497
- private _buildDetailsPage(): HTMLElement | null {
498
- if (!this._hasDetails()) return null;
499
- const { page, body } = this._buildPage('details', 'Details', false);
500
-
501
- if (this.comments) {
581
+ if (visibility.commentsLabel) {
502
582
  const comments = document.createElement('p');
503
583
  comments.className = 'adw-about-dialog-comments';
504
584
  comments.textContent = this.comments;
505
585
  body.appendChild(comments);
506
586
  }
507
587
 
508
- const links: Array<[string, string]> = [];
509
- if (this.website) links.push(['Website', this.website]);
510
- if (this.supportUrl) links.push(['Support Questions', this.supportUrl]);
511
- if (this.issueUrl) links.push(['Report an Issue', this.issueUrl]);
512
-
513
- if (links.length > 0) {
514
- const group = document.createElement('adw-preferences-group');
515
- for (const [title, uri] of links) group.appendChild(this._buildLinkRow(title, uri));
516
- const wrap = document.createElement('div');
517
- wrap.className = 'adw-about-dialog-rows';
518
- wrap.appendChild(group);
519
- body.appendChild(wrap);
588
+ if (visibility.linksGroup) {
589
+ const rows: HTMLElement[] = [];
590
+ if (visibility.detailsWebsiteRow) {
591
+ rows.push(this._buildLinkRow(rowLabel('websiteRow'), this.website));
592
+ }
593
+ body.appendChild(this._buildGroup('links', rows));
520
594
  }
521
595
 
522
596
  return page;
@@ -529,73 +603,54 @@ export class AdwAboutDialog extends HTMLElement {
529
603
  row.setAttribute('activatable', '');
530
604
  row.classList.add('adw-about-dialog-link-row');
531
605
 
532
- const chevron = document.createElement('span');
606
+ const chevron = createAdwIcon('go-next', 'adw-about-dialog-chevron');
533
607
  chevron.setAttribute('slot', 'suffix');
534
- chevron.className = 'adw-icon adw-icon--go-next adw-about-dialog-chevron';
535
- chevron.setAttribute('aria-hidden', 'true');
536
608
  row.appendChild(chevron);
537
609
 
538
- row.addEventListener('activated', () => {
539
- this.dispatchEvent(new CustomEvent('activate-link', { bubbles: true, detail: { uri } }));
540
- window.open(uri, '_blank', 'noopener,noreferrer');
541
- });
610
+ row.addEventListener('activated', () => this._activateLink(uri));
542
611
 
543
612
  return row;
544
613
  }
545
614
 
546
- /** The Credits page: a group per non-empty credit section. */
547
- private _buildCreditsPage(): HTMLElement | null {
548
- if (!this._hasCredits()) return null;
549
- const { page, body } = this._buildPage('credits', 'Credits', false);
550
-
551
- const sections: Array<[string, string[]]> = [
552
- ['Code by', this._developers],
553
- ['Design by', this._designers],
554
- ['Artwork by', this._artists],
555
- ['Documentation by', this._documenters],
556
- ];
615
+ /** The Credits page: one group per section the core assembled. */
616
+ private _buildCreditsPage(sections: ReturnType<typeof creditsSections>): HTMLElement {
617
+ const { page, body } = this._buildPage('credits', ADW_ABOUT_DIALOG_LABELS.creditsPage, false);
557
618
 
558
- for (const [title, people] of sections) {
559
- if (people.length === 0) continue;
560
- const group = document.createElement('adw-preferences-group');
561
- group.setAttribute('title', title);
562
- for (const person of people) group.appendChild(this._buildCreditRow(person));
563
- const wrap = document.createElement('div');
564
- wrap.className = 'adw-about-dialog-rows';
565
- wrap.appendChild(group);
566
- body.appendChild(wrap);
619
+ for (const section of sections) {
620
+ const rows = section.people.map((person) => this._buildCreditRow(person));
621
+ body.appendChild(this._buildGroup(null, rows, section.title ?? undefined));
567
622
  }
568
623
 
569
624
  return page;
570
625
  }
571
626
 
572
- /** A credit row — a plain title, or a link row when the entry carries a URI. */
573
- private _buildCreditRow(raw: string): HTMLElement {
574
- const { name, uri } = parseCredit(raw);
627
+ /**
628
+ * A credit row — a plain title, or a link row when the entry carries a URI.
629
+ *
630
+ * The gate is `link !== null`, not the URI's truthiness: `"Ada <>"` parses to
631
+ * an EMPTY link, which is still a link in the C (`if (link)` tests the
632
+ * pointer, :562) and still produces an `AdwLinkRow`, to `mailto:`.
633
+ */
634
+ private _buildCreditRow(person: AdwCreditPerson): HTMLElement {
575
635
  const row = document.createElement('adw-action-row');
576
- row.setAttribute('title', name);
636
+ row.setAttribute('title', person.name);
577
637
 
578
- if (uri) {
638
+ if (person.link !== null) {
639
+ const uri = person.uri ?? '';
579
640
  row.setAttribute('activatable', '');
580
641
  row.classList.add('adw-about-dialog-link-row');
581
- const chevron = document.createElement('span');
642
+ const chevron = createAdwIcon('go-next', 'adw-about-dialog-chevron');
582
643
  chevron.setAttribute('slot', 'suffix');
583
- chevron.className = 'adw-icon adw-icon--go-next adw-about-dialog-chevron';
584
- chevron.setAttribute('aria-hidden', 'true');
585
644
  row.appendChild(chevron);
586
- row.addEventListener('activated', () => {
587
- this.dispatchEvent(new CustomEvent('activate-link', { bubbles: true, detail: { uri } }));
588
- window.open(uri, '_blank', 'noopener,noreferrer');
589
- });
645
+ row.addEventListener('activated', () => this._activateLink(uri));
590
646
  }
591
647
 
592
648
  return row;
593
649
  }
594
650
 
595
651
  /** The Legal page: copyright + license text. */
596
- private _buildLegalPage(): HTMLElement | null {
597
- if (!this._hasLegal()) return null;
598
- const { page, body } = this._buildPage('legal', 'Legal', false);
652
+ private _buildLegalPage(): HTMLElement {
653
+ const { page, body } = this._buildPage('legal', ADW_ABOUT_DIALOG_LABELS.legalPage, false);
599
654
 
600
655
  if (this.copyright) {
601
656
  const copyright = document.createElement('p');
@@ -608,11 +663,16 @@ export class AdwAboutDialog extends HTMLElement {
608
663
  const license = document.createElement('p');
609
664
  license.className = 'adw-about-dialog-license';
610
665
  if (this.licenseUrl) {
666
+ // GTK connects the legal label's `activate-link` to the same
667
+ // handler as the rows (:695), so this link is cancelable too.
611
668
  const link = document.createElement('a');
612
669
  link.href = this.licenseUrl;
613
- link.target = '_blank';
614
670
  link.rel = 'noopener noreferrer';
615
671
  link.textContent = this.license;
672
+ link.addEventListener('click', (event) => {
673
+ event.preventDefault();
674
+ this._activateLink(this.licenseUrl);
675
+ });
616
676
  license.appendChild(link);
617
677
  } else {
618
678
  license.textContent = this.license;