@gjsify/adwaita-web 0.64.1 → 0.64.2-next.252

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 (83) hide show
  1. package/README.md +12 -8
  2. package/dist/adwaita-web.css +39 -9
  3. package/dist/adwaita-web.css.map +1 -1
  4. package/lib/types/application.d.ts +1 -1
  5. package/lib/types/child-slot.spec.d.ts +1 -0
  6. package/lib/types/container-fill.spec.d.ts +1 -0
  7. package/lib/types/elements/adw-alert-dialog.d.ts +77 -17
  8. package/lib/types/elements/adw-toast-overlay.d.ts +8 -29
  9. package/lib/types/elements/adw-window-features.d.ts +8 -0
  10. package/lib/types/elements/gtk-application-window.d.ts +1 -4
  11. package/lib/types/elements/gtk-emoji-chooser.d.ts +2 -0
  12. package/lib/types/elements/gtk-popover-menu.d.ts +2 -0
  13. package/lib/types/elements/gtk-popover.d.ts +5 -0
  14. package/lib/types/elements/gtk-stack-page.d.ts +28 -0
  15. package/lib/types/elements/gtk-stack.d.ts +11 -0
  16. package/lib/types/elements/gtk-window.d.ts +16 -1
  17. package/lib/types/gobject-elements.d.ts +9 -0
  18. package/lib/types/index.d.ts +2 -2
  19. package/lib/types/learn6502-window.spec.d.ts +1 -0
  20. package/lib/types/namespace/adw.d.ts +1 -1
  21. package/lib/types/namespace/gtk.d.ts +2 -0
  22. package/lib/types/shared-tree-builder.d.ts +31 -0
  23. package/lib/types/styles.generated.d.ts +1 -1
  24. package/lib/types/widget-visible.spec.d.ts +1 -0
  25. package/lib/types/window-maximize.spec.d.ts +1 -0
  26. package/package.json +13 -12
  27. package/scss/_bin.scss +6 -3
  28. package/scss/_overlay.scss +11 -1
  29. package/scss/_reset.scss +1 -1
  30. package/scss/_stack.scss +8 -2
  31. package/scss/_toast.scss +13 -3
  32. package/scss/_window.scss +16 -0
  33. package/src/adw-alert-dialog.spec.ts +71 -5
  34. package/src/adw-bin.spec.ts +4 -3
  35. package/src/adw-header-bar.spec.ts +18 -0
  36. package/src/adw-toast-overlay.spec.ts +68 -24
  37. package/src/application.spec.ts +44 -0
  38. package/src/application.ts +13 -18
  39. package/src/capabilities.mjs +1 -3
  40. package/src/child-slot.spec.ts +71 -0
  41. package/src/construct-vectors.spec.ts +51 -6
  42. package/src/container-fill.spec.ts +70 -0
  43. package/src/elements/adw-alert-dialog.ts +174 -44
  44. package/src/elements/adw-application-window.ts +0 -3
  45. package/src/elements/adw-breakpoint-bin.ts +3 -0
  46. package/src/elements/adw-dialog.ts +3 -1
  47. package/src/elements/adw-header-bar.ts +4 -0
  48. package/src/elements/adw-toast-overlay.ts +35 -69
  49. package/src/elements/adw-window-features.ts +14 -2
  50. package/src/elements/gtk-application-window.ts +3 -8
  51. package/src/elements/gtk-drag-icon.ts +4 -0
  52. package/src/elements/gtk-emoji-chooser.ts +5 -0
  53. package/src/elements/gtk-flow-box-child.ts +3 -1
  54. package/src/elements/gtk-graphics-offload.ts +3 -1
  55. package/src/elements/gtk-list-box-row.ts +3 -1
  56. package/src/elements/gtk-overlay.ts +5 -4
  57. package/src/elements/gtk-paned.ts +7 -1
  58. package/src/elements/gtk-popover-bin.ts +3 -0
  59. package/src/elements/gtk-popover-menu.ts +5 -0
  60. package/src/elements/gtk-popover.ts +12 -0
  61. package/src/elements/gtk-search-bar.ts +5 -5
  62. package/src/elements/gtk-stack-page.ts +111 -0
  63. package/src/elements/gtk-stack.ts +50 -5
  64. package/src/elements/gtk-tree-expander.ts +3 -0
  65. package/src/elements/gtk-viewport.ts +4 -0
  66. package/src/elements/gtk-window-handle.ts +4 -0
  67. package/src/elements/gtk-window.ts +55 -6
  68. package/src/gobject-elements.ts +28 -0
  69. package/src/gtk-stack.spec.ts +170 -0
  70. package/src/index.ts +2 -2
  71. package/src/keyboard-operable.spec.ts +3 -3
  72. package/src/learn6502-window.spec.ts +113 -0
  73. package/src/namespace/adw.ts +1 -1
  74. package/src/namespace/gtk.ts +2 -0
  75. package/src/shared-tree-builder.ts +105 -10
  76. package/src/shared-trees.spec.ts +15 -1
  77. package/src/slotted-children.spec.ts +24 -0
  78. package/src/slotted-children.ts +18 -2
  79. package/src/styles.generated.ts +1 -1
  80. package/src/test.browser.mts +10 -0
  81. package/src/value-lists.spec.ts +3 -3
  82. package/src/widget-visible.spec.ts +104 -0
  83. package/src/window-maximize.spec.ts +93 -0
@@ -0,0 +1,113 @@
1
+ // Learn6502's `main.window.blp`, reduced to the shapes that rendered short through
2
+ // `buildWithSiblings`: header-bar buttons after a single `[start]` (GtkBuilder packs an untyped
3
+ // child at start too), and `Overlay { child: Adw.ToastOverlay { Stack } }`. Every
4
+ // widget the file authors has to be in the document once the built window is attached — the
5
+ // failure was widgets that were built and then silently dropped on the way.
6
+ import { describe, expect, it } from '@gjsify/unit';
7
+ import { parseBlueprint, projectToSharedNode } from '@gjsify/blueprint';
8
+
9
+ import { buildWithSiblings } from './shared-tree-builder.js';
10
+
11
+ const BLUEPRINT = `using Gtk 4.0;
12
+ using Adw 1;
13
+
14
+ Adw.ApplicationWindow mainWindow {
15
+ content: Adw.ToolbarView toolbarView {
16
+ [top]
17
+ Adw.HeaderBar headerBar {
18
+ title-widget: Label titleLabel {
19
+ label: "Learn 6502";
20
+ };
21
+
22
+ [start]
23
+ ToggleButton sidebarToggleButton {
24
+ icon-name: "sidebar-show-symbolic";
25
+ }
26
+
27
+ Button learnBackButton {
28
+ icon-name: "go-previous-symbolic";
29
+ }
30
+
31
+ Button unsavedChangesIndicator {
32
+ icon-name: "dot-symbolic";
33
+ }
34
+
35
+ [end]
36
+ MenuButton menuButton {
37
+ icon-name: "open-menu-symbolic";
38
+ }
39
+ }
40
+
41
+ content: Overlay mainOverlay {
42
+ child: Adw.ToastOverlay toastOverlay {
43
+ Stack layoutHost {
44
+ StackPage singlePage {
45
+ name: "single";
46
+ child: Adw.Bin singleContainer {
47
+ child: Label singleLabel {
48
+ label: "single";
49
+ };
50
+ };
51
+ }
52
+
53
+ StackPage threePage {
54
+ name: "three";
55
+ child: Adw.Bin threeColumnContainer {
56
+ child: Box threeColumns {
57
+ Label threeLabel {
58
+ label: "three";
59
+ }
60
+ };
61
+ };
62
+ }
63
+ }
64
+ };
65
+ };
66
+ };
67
+ }
68
+ `;
69
+
70
+ const AUTHORED = [
71
+ 'toolbarView',
72
+ 'headerBar',
73
+ 'titleLabel',
74
+ 'sidebarToggleButton',
75
+ 'learnBackButton',
76
+ 'unsavedChangesIndicator',
77
+ 'menuButton',
78
+ 'mainOverlay',
79
+ 'toastOverlay',
80
+ 'layoutHost',
81
+ 'singleContainer',
82
+ 'singleLabel',
83
+ 'threeColumnContainer',
84
+ 'threeColumns',
85
+ 'threeLabel',
86
+ ];
87
+
88
+ export const AdwLearn6502WindowTest = async () => {
89
+ await describe("Learn6502's main window through buildWithSiblings", async () => {
90
+ const { node, lost } = projectToSharedNode(parseBlueprint(BLUEPRINT, 'main.window.blp'));
91
+
92
+ await it('projects without losing anything', () => {
93
+ expect(lost.map((loss) => `${loss.kind}@${loss.line}`).join()).toBe('');
94
+ });
95
+
96
+ await it('puts every authored widget in the document, the start buttons at start', () => {
97
+ const { root } = buildWithSiblings(node);
98
+ document.body.append(root);
99
+ const missing = AUTHORED.filter((id) => !root.querySelector(`#${id}`)?.isConnected);
100
+ const start = ['sidebarToggleButton', 'learnBackButton', 'unsavedChangesIndicator'].filter(
101
+ (id) => root.querySelector(`#${id}`)?.parentElement?.className !== 'adw-header-bar-start',
102
+ );
103
+ const toast = root.querySelector('#toastOverlay');
104
+ const overlaid =
105
+ toast?.closest('.adw-overlay-child') === root.querySelector('#mainOverlay > .adw-overlay-child');
106
+ root.remove();
107
+
108
+ expect(missing.join()).toBe('');
109
+ expect(start.join()).toBe('');
110
+ expect(overlaid).toBe(true);
111
+ });
112
+ });
113
+ };
@@ -111,4 +111,4 @@ export { AdwWindowTitle as WindowTitle } from '../elements/adw-window-title.js';
111
111
  export { AdwWrapBox as WrapBox } from '../elements/adw-wrap-box.js';
112
112
  export { AdwApplication as Application } from '../application.js';
113
113
  export { AdwStyleManager as StyleManager } from '../style-manager.js';
114
- export { ColorScheme } from '@gjsify/adwaita-core';
114
+ export { ColorScheme, Toast, ToastPriority } from '@gjsify/adwaita-core';
@@ -86,6 +86,7 @@ export { GtkSeparator as Separator } from '../elements/gtk-separator.js';
86
86
  export { GtkSpinButton as SpinButton } from '../elements/gtk-spin-button.js';
87
87
  export { GtkSpinner as Spinner } from '../elements/gtk-spinner.js';
88
88
  export { GtkStack as Stack } from '../elements/gtk-stack.js';
89
+ export { GtkStackPage as StackPage } from '../elements/gtk-stack-page.js';
89
90
  export { GtkStackSidebar as StackSidebar } from '../elements/gtk-stack-sidebar.js';
90
91
  export { GtkStackSwitcher as StackSwitcher } from '../elements/gtk-stack-switcher.js';
91
92
  export { GtkStringList as StringList } from '@gjsify/adwaita-core';
@@ -101,6 +102,7 @@ export type { WindowCommon } from '@gjsify/adwaita-core';
101
102
  export { GtkWindowControls as WindowControls } from '../elements/gtk-window-controls.js';
102
103
  export { GtkViewport as Viewport } from '../elements/gtk-viewport.js';
103
104
  export { GtkWindowHandle as WindowHandle } from '../elements/gtk-window-handle.js';
105
+ export { ApplicationInhibitFlags } from '@gjsify/adwaita-core';
104
106
  export { GtkApplication as Application } from '../application.js';
105
107
  export {
106
108
  STYLE_PROVIDER_PRIORITY_APPLICATION,
@@ -55,7 +55,14 @@ import {
55
55
 
56
56
  import { observeAdaptiveSize } from './breakpoints.js';
57
57
  import { capabilities } from './capabilities.mjs';
58
- import { endpointOf, hasElementApi, installElementApi, installSensitive } from './gobject-elements.js';
58
+ import {
59
+ endpointOf,
60
+ hasElementApi,
61
+ hasInstalledVisible,
62
+ installElementApi,
63
+ installSensitive,
64
+ installVisible,
65
+ } from './gobject-elements.js';
59
66
  import { dispatchedSignalsOf } from './signals.js';
60
67
  import { slottedChildrenOf } from './slotted-children.js';
61
68
  import { isTemplateTag, templateTagFor } from './template-classes.js';
@@ -154,14 +161,93 @@ function isWritable(el: object, member: string): boolean {
154
161
  * upgraded has declared no slots yet, so the refusal below belongs to the mount.
155
162
  */
156
163
  export function buildSharedTree(node: SharedTreeNode, record: BuildRecord = newRecord()): HTMLElement {
164
+ refuseSiblings(node, 'buildSharedTree');
165
+ return buildTree(node, record).root;
166
+ }
167
+
168
+ /** What {@link buildWithSiblings} hands back: the root element and the objects built beside it. */
169
+ export interface BuiltTree {
170
+ /** The element the tree roots at, as {@link buildSharedTree} returns it. */
171
+ root: HTMLElement;
172
+ /**
173
+ * The sibling objects by id — `Adw.AlertDialog unsavedChangesDialog { }` or `$Learn learn { }`
174
+ * written after the template. A sibling is built in the root's id scope (a setter or a bind
175
+ * may name it) but is no child of the root: the code beside the file decides where, or
176
+ * whether, it is placed.
177
+ */
178
+ siblings: ReadonlyMap<string, HTMLElement>;
179
+ /** Every element the tree names by id, siblings included: what `GtkBuilder.get_object` answers for. */
180
+ objects: ReadonlyMap<string, HTMLElement>;
181
+ }
182
+
183
+ /** What {@link buildWithSiblings} takes beside the tree. */
184
+ export interface BuildOptions {
185
+ /** The object a handler NAME is looked up on (ADR 0093 § 3). */
186
+ scope?: Readonly<Record<string, unknown>>;
187
+ /** The size source a `breakpoints` node is driven from. */
188
+ observeSize?: SizeSource;
189
+ }
190
+
191
+ /**
192
+ * A tree with sibling roots (ADR 0093): the root element, and each sibling by id, all DETACHED
193
+ * like {@link buildSharedTree}'s result.
194
+ *
195
+ * {@link buildSharedTree}, {@link mountSharedTree} and {@link buildTemplateTree} refuse such a
196
+ * tree by name, because they return one element and the siblings would be built and then thrown
197
+ * away. A sibling with no id is refused too: nothing could ever ask for it, which is how a
198
+ * dropped object looks finished.
199
+ */
200
+ export function buildWithSiblings(node: SharedTreeNode, options: BuildOptions = {}): BuiltTree {
201
+ const record = newRecord(options.scope, options.observeSize);
202
+ const built = buildTree(node, record);
203
+ // An element declares its slots when it is connected, so a detached tree cannot be checked
204
+ // as it stands. It is connected for the length of the check and handed back detached, as
205
+ // documented: a placement the renderer could not honour is refused here, like
206
+ // `mountSharedTree` refuses it, instead of surfacing as a widget missing from the page.
207
+ const probe = document.createElement('div');
208
+ probe.hidden = true;
209
+ probe.append(built.root, ...built.siblings.values());
210
+ document.body.append(probe);
211
+ try {
212
+ refuseUnknownSlots(record.placed);
213
+ refuseUnheldExtensions(record.extended);
214
+ } catch (error) {
215
+ for (const dispose of record.disposers) dispose();
216
+ throw error;
217
+ } finally {
218
+ probe.remove();
219
+ for (const el of [built.root, ...built.siblings.values()]) el.remove();
220
+ }
221
+ return built;
222
+ }
223
+
224
+ function refuseSiblings(node: SharedTreeNode, door: string): void {
225
+ if ((node.siblings?.length ?? 0) === 0) return;
226
+ throw new Error(
227
+ `\`${node.tag}\` carries ${node.siblings!.length} sibling object(s), and \`${door}\` returns one object ` +
228
+ 'so they would be built and lost. Use `buildWithSiblings`, which hands them back by id.',
229
+ );
230
+ }
231
+
232
+ /** Every node, its siblings in the same id scope, then the binds and breakpoints that name them. */
233
+ function buildTree(node: SharedTreeNode, record: BuildRecord): BuiltTree {
157
234
  // ADR 0093 § 2: the whole tree is checked against the capability table before an element is
158
235
  // created. A refused `layout` is named with its node and the reason the table gives.
159
236
  assertTreeConstructs('adwaita-web', capabilities, node);
160
237
  if (node.menus !== undefined) record.menus = node.menus;
161
238
  const root = buildNode(node, record);
239
+ const siblings = new Map<string, HTMLElement>();
240
+ for (const sibling of node.siblings ?? []) {
241
+ if (sibling.id === undefined) {
242
+ throw new Error(
243
+ `the sibling \`${sibling.tag}\` has no id, so nothing could ever ask for it: give it one in the .blp.`,
244
+ );
245
+ }
246
+ siblings.set(sibling.id, buildNode(sibling, record));
247
+ }
162
248
  for (const bind of record.binds) bindProperty(bind, record);
163
249
  for (const host of record.breakpointHosts) bindBreakpoints(host.el, host.node, record);
164
- return root;
250
+ return { root, siblings, objects: new Map(record.ids) };
165
251
  }
166
252
 
167
253
  /** A fresh record, with the scope when the caller has one. */
@@ -344,7 +430,9 @@ export function writeProp(el: HTMLElement, prop: string, value: string | number
344
430
  const member = propertyOf(prop);
345
431
  const expand = GTK_WIDGET_EXPAND.find((name) => name === attr);
346
432
  if (expand !== undefined && typeof value === 'boolean') el.setAttribute(expand, value ? '' : 'false');
347
- else if (isTemplateTag(el.localName) && member in el) {
433
+ else if (attr === 'visible' && typeof value === 'boolean' && hasInstalledVisible(el)) {
434
+ (el as unknown as Record<string, unknown>)[member] = value;
435
+ } else if (isTemplateTag(el.localName) && member in el) {
348
436
  // An application class has no attributeChangedCallback: its properties are GObject properties,
349
437
  // set the way `Gtk.Builder` sets them, after the constructor.
350
438
  (el as unknown as Record<string, unknown>)[member] = value;
@@ -372,9 +460,14 @@ function buildNode(node: SharedTreeNode, record: BuildRecord): HTMLElement {
372
460
  const el = document.createElement(node.extern === true ? templateTagFor(node.tag) : hostTagOf(node.tag));
373
461
  // A template's children talk GObject to the template: `connect`, `emit`, `notify` (ADR 0096).
374
462
  if (record.template !== undefined) installElementApi(el);
463
+ // `Gtk.Widget:visible`, which no element spells: a setter's target and an authored `false` need it.
464
+ installVisible(el);
375
465
  // The id is how the TypeScript beside a `.blp` reaches this element
376
466
  // (`root.querySelector('#…')`), the counterpart of `InternalChildren` on GTK.
377
467
  if (node.id !== undefined) {
468
+ if (record.ids.has(node.id)) {
469
+ throw new Error(`Two nodes of this tree carry the id '${node.id}'; GtkBuilder refuses a duplicate id.`);
470
+ }
378
471
  el.id = node.id;
379
472
  record.ids.set(node.id, el);
380
473
  }
@@ -492,6 +585,7 @@ export interface BuiltTemplateTree {
492
585
  * belong to the first `connectedCallback`.
493
586
  */
494
587
  export function buildTemplateTree(tree: SharedTreeNode, scope: TemplateScope, host: HTMLElement): BuiltTemplateTree {
588
+ refuseSiblings(tree, 'buildTemplateTree');
495
589
  assertTreeConstructs('adwaita-web', capabilities, tree);
496
590
  const record = newRecord(undefined, undefined, scope);
497
591
  record.menus = tree.menus;
@@ -591,9 +685,9 @@ function writeExtensions(el: HTMLElement, node: SharedTreeNode): void {
591
685
 
592
686
  /** The response API an element answers to — `<adw-alert-dialog>`'s, named after libadwaita's. */
593
687
  interface ResponseReader {
594
- getResponseLabel(id: string): string | null;
595
- getResponseAppearance(id: string): string | null;
596
- getResponseEnabled(id: string): boolean;
688
+ get_response_label(id: string): string | null;
689
+ get_response_appearance(id: string): string | null;
690
+ get_response_enabled(id: string): boolean;
597
691
  }
598
692
 
599
693
  /**
@@ -617,10 +711,10 @@ function refuseUnheldExtensions(extended: readonly ExtendedNode[]): void {
617
711
  const reader = el as unknown as Partial<ResponseReader>;
618
712
  for (const response of node.extensions?.responses ?? []) {
619
713
  const held =
620
- typeof reader.getResponseLabel === 'function' &&
621
- reader.getResponseLabel(response.id) === response.label &&
622
- reader.getResponseAppearance?.(response.id) === (response.appearance ?? 'default') &&
623
- reader.getResponseEnabled?.(response.id) === (response.enabled ?? true);
714
+ typeof reader.get_response_label === 'function' &&
715
+ reader.get_response_label(response.id) === response.label &&
716
+ reader.get_response_appearance?.(response.id) === (response.appearance ?? 'default') &&
717
+ reader.get_response_enabled?.(response.id) === (response.enabled ?? true);
624
718
  if (!held) {
625
719
  throw new Error(
626
720
  `<${el.localName}> did not register the response "${response.id}" as authored, so the ` +
@@ -696,6 +790,7 @@ export interface MountOptions {
696
790
  * `buildSharedTree` is for a caller that already has somewhere of its own to attach it.
697
791
  */
698
792
  export function mountSharedTree(node: SharedTreeNode, options: MountOptions = {}): MountedSharedTree {
793
+ refuseSiblings(node, 'mountSharedTree');
699
794
  const { into } = options;
700
795
  const host = into ?? document.createElement('div');
701
796
  const record = newRecord(options.scope, options.observeSize);
@@ -54,7 +54,7 @@ import { ADWAITA_GALLERY_SHARED_TREES } from '../../../../scripts/adwaita-galler
54
54
 
55
55
  import '@gjsify/adwaita-web';
56
56
 
57
- import { mountSharedTree } from './shared-tree-builder.js';
57
+ import { buildWithSiblings, mountSharedTree } from './shared-tree-builder.js';
58
58
 
59
59
  /**
60
60
  * Connect the authored root — these elements build on connect — run, and take it down again.
@@ -364,6 +364,20 @@ export const AdwSharedTreesTest = async () => {
364
364
  ).toThrow('has no slot "middle"');
365
365
  });
366
366
 
367
+ // The detached door refuses it too: a root built and handed back is mounted by the
368
+ // caller, after the builder could still say anything.
369
+ await it('`buildWithSiblings` refuses the same placement, and hands a good tree back detached', () => {
370
+ const bad = { tag: 'AdwToolbarView', children: [{ tag: 'AdwBanner', slot: 'middle' }] };
371
+ expect(() => buildWithSiblings(bad)).toThrow('has no slot "middle"');
372
+ const { root, siblings } = buildWithSiblings({
373
+ tag: 'AdwToolbarView',
374
+ children: [{ tag: 'AdwBanner', slot: 'top' }],
375
+ siblings: [{ tag: 'GtkLabel', id: 'beside' }],
376
+ });
377
+ expect(root.isConnected || siblings.get('beside')!.isConnected).toBe(false);
378
+ expect(root.querySelector('adw-banner') !== null).toBe(true);
379
+ });
380
+
367
381
  // ADR 0092's `layout { }`: a grid child's cell. GTK counts from 0, CSS Grid from 1.
368
382
  await it('`layout { }` places a grid child in its cell, spans defaulting to one', () => {
369
383
  mounted(
@@ -113,6 +113,30 @@ export const AdwSlottedChildrenTest = async () => {
113
113
  expect(report).toBe('[1]');
114
114
  });
115
115
 
116
+ // The DECLARED twin of the case above. `install()` replaces the host's children with
117
+ // its structure, and a child no slot took used to go with them — gone without a word.
118
+ await it('keeps a declared child no slot takes, and warns that it was not placed', () => {
119
+ const host = document.createElement('div');
120
+ const typo = probe('sied', 'a');
121
+ host.append(document.createTextNode('\n '), typo);
122
+ mounted(host);
123
+ const side = document.createElement('div');
124
+ side.className = 'side';
125
+
126
+ const warned: string[] = [];
127
+ const warn = console.warn;
128
+ console.warn = (...args: unknown[]) => void warned.push(args.join(' '));
129
+ try {
130
+ bindSlottedChildren(host, [{ name: 'side', into: side }]).install(side);
131
+ } finally {
132
+ console.warn = warn;
133
+ }
134
+
135
+ const report = `${placement(host, typo)} | ${warned.join()}`;
136
+ host.remove();
137
+ expect(report).toBe("[1] | 'sied' is not a valid child type of 'div'");
138
+ });
139
+
116
140
  await it('re-adopts a child that was appended, removed and appended again', async () => {
117
141
  const host = mounted(document.createElement('div'));
118
142
  const side = document.createElement('div');
@@ -113,6 +113,12 @@ export interface AdwSlottedChildren {
113
113
  /** Where a bound host's slots can be read back from — see {@link slottedChildrenOf}. */
114
114
  const BOUND = new WeakMap<Element, AdwSlottedChildren>();
115
115
 
116
+ /** An element or a text node with something in it — markup's indentation and comments are not. */
117
+ function isContent(node: Node): boolean {
118
+ if (node instanceof Element) return true;
119
+ return node.nodeType === Node.TEXT_NODE && (node.textContent ?? '').trim() !== '';
120
+ }
121
+
116
122
  /**
117
123
  * The slot binding `host` installed, if it has one.
118
124
  *
@@ -184,9 +190,19 @@ export function bindSlottedChildren(
184
190
  slots,
185
191
  slotFor,
186
192
  install(...nodes: readonly Node[]) {
187
- for (const node of Array.from(host.childNodes)) route(node);
193
+ const unplaced: Node[] = [];
194
+ for (const node of Array.from(host.childNodes)) {
195
+ if (route(node) !== null || node.parentNode !== host || !isContent(node)) continue;
196
+ // `replaceChildren` below would delete it, and a child the author wrote that
197
+ // vanishes without a word is the silent drop this used to be. It stays beside
198
+ // the structure, as a late child with the same slot does, and says so the
199
+ // way GtkBuilder does (GTK_BUILDER_WARN_INVALID_CHILD_TYPE).
200
+ unplaced.push(node);
201
+ const type = node instanceof Element ? (node.getAttribute('slot') ?? '') : '';
202
+ console.warn(`'${type}' is not a valid child type of '${host.localName}'`);
203
+ }
188
204
  structure = new Set(nodes);
189
- host.replaceChildren(...nodes);
205
+ host.replaceChildren(...nodes, ...unplaced);
190
206
  // Armed AFTER `replaceChildren`, so its own records are never delivered: an
191
207
  // observer armed first would see the whole of `structure` as added nodes and
192
208
  // spend a microtask rejecting them.