@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.
- package/dist/adwaita-web.css +255 -27
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/blueprint-layout.spec.d.ts +1 -0
- package/lib/types/blueprint-markup.spec.d.ts +4 -0
- package/lib/types/blueprint-tree.spec.d.ts +1 -0
- package/lib/types/elements/adw-bottom-sheet.d.ts +31 -13
- package/lib/types/elements/adw-carousel.d.ts +5 -0
- package/lib/types/elements/adw-header-bar.d.ts +15 -0
- package/lib/types/elements/adw-inline-view-switcher.d.ts +1 -0
- package/lib/types/elements/adw-navigation-view.d.ts +1 -0
- package/lib/types/elements/adw-sidebar.d.ts +10 -0
- package/lib/types/elements/adw-tab-view.d.ts +1 -0
- package/lib/types/elements/adw-toggle-group.d.ts +11 -0
- package/lib/types/elements/gtk-action-bar.d.ts +19 -0
- package/lib/types/elements/gtk-box.d.ts +17 -0
- package/lib/types/elements/gtk-button.d.ts +2 -0
- package/lib/types/elements/gtk-label.d.ts +122 -0
- package/lib/types/gio-menu.spec.d.ts +1 -0
- package/lib/types/gtk-box.spec.d.ts +1 -0
- package/lib/types/gtk-label.spec.d.ts +1 -0
- package/lib/types/gtk-value-doors.spec.d.ts +1 -0
- package/lib/types/index.d.ts +4 -1
- package/lib/types/namespace/gio.d.ts +1 -0
- package/lib/types/namespace/gtk.d.ts +5 -0
- package/lib/types/shared-tree-builder.d.ts +64 -0
- package/lib/types/source-view/asm6502.d.ts +1 -20
- package/lib/types/source-view/index.d.ts +3 -2
- package/lib/types/source-view/theme.d.ts +1 -1
- package/lib/types/string-list-slot.d.ts +4 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/lib/types/tags.spec.d.ts +1 -0
- package/lib/types/value-lists.spec.d.ts +1 -0
- package/package.json +8 -6
- package/scss/_action_bar.scss +51 -0
- package/scss/_banner.scss +8 -1
- package/scss/_bottom_sheet.scss +57 -0
- package/scss/_box.scss +23 -0
- package/scss/_button.scss +29 -7
- package/scss/_button_content.scss +6 -1
- package/scss/_button_row.scss +5 -2
- package/scss/_carousel.scss +6 -0
- package/scss/_labels.scss +126 -12
- package/scss/_navigation_split_view.scss +18 -0
- package/scss/_overlay_split_view.scss +5 -0
- package/scss/_reset.scss +29 -4
- package/scss/_split_button.scss +5 -2
- package/scss/_status_page.scss +31 -17
- package/scss/_theme.scss +11 -3
- package/scss/_toolbar_view.scss +10 -0
- package/scss/_variables.scss +1 -0
- package/scss/_widget.scss +55 -0
- package/scss/_window_title.scss +9 -0
- package/scss/adwaita-skin.scss +8 -2
- package/src/adw-banner.spec.ts +16 -0
- package/src/adw-button-row.spec.ts +8 -0
- package/src/adw-header-bar.spec.ts +17 -0
- package/src/blueprint-layout.spec.ts +223 -0
- package/src/blueprint-markup.spec.ts +155 -0
- package/src/blueprint-tree.spec.ts +174 -0
- package/src/bottom-sheet.spec.ts +166 -1
- package/src/chrome.spec.ts +5 -2
- package/src/elements/adw-bottom-sheet.ts +166 -59
- package/src/elements/adw-carousel.ts +10 -5
- package/src/elements/adw-clamp.ts +12 -0
- package/src/elements/adw-combo-row.ts +13 -4
- package/src/elements/adw-header-bar.ts +46 -3
- package/src/elements/adw-inline-view-switcher.ts +11 -0
- package/src/elements/adw-navigation-view.ts +10 -0
- package/src/elements/adw-sidebar.ts +22 -0
- package/src/elements/adw-spin-row.ts +44 -5
- package/src/elements/adw-status-page.ts +12 -6
- package/src/elements/adw-tab-view.ts +20 -3
- package/src/elements/adw-toggle-group.ts +45 -4
- package/src/elements/adw-toolbar-view.ts +9 -0
- package/src/elements/gtk-action-bar.ts +90 -0
- package/src/elements/gtk-box.ts +87 -0
- package/src/elements/gtk-button.ts +60 -13
- package/src/elements/gtk-drop-down.ts +10 -1
- package/src/elements/gtk-label.ts +415 -0
- package/src/gio-menu.spec.ts +125 -0
- package/src/globals.d.ts +7 -0
- package/src/gtk-box.spec.ts +168 -0
- package/src/gtk-button.spec.ts +123 -1
- package/src/gtk-label.spec.ts +595 -0
- package/src/gtk-value-doors.spec.ts +104 -0
- package/src/index.ts +14 -1
- package/src/keyboard-operable.spec.ts +2 -1
- package/src/namespace/gio.ts +19 -0
- package/src/namespace/gtk.ts +14 -0
- package/src/shared-tree-builder.ts +268 -0
- package/src/shared-trees.spec.ts +296 -32
- package/src/source-view/adw-source-view.ts +3 -3
- package/src/source-view/asm6502.ts +11 -191
- package/src/source-view/index.ts +17 -11
- package/src/source-view/theme.ts +16 -16
- package/src/split-button.spec.ts +7 -0
- package/src/string-list-slot.ts +21 -0
- package/src/styles.generated.ts +1 -1
- package/src/tags.spec.ts +51 -0
- package/src/test.browser.mts +18 -0
- package/src/value-lists.spec.ts +93 -0
- package/lib/types/source-view/hex.d.ts +0 -21
- package/src/source-view/hex.ts +0 -37
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// `Gtk.StringList` and `Gtk.Adjustment` AT THE ELEMENT, held against the spelling they
|
|
2
|
+
// replace.
|
|
3
|
+
//
|
|
4
|
+
// THE CLAIM UNDER TEST is backward compatibility, and it is the one claim a core-side spec
|
|
5
|
+
// cannot make. `@gjsify/adwaita-core` proves the two classes reduce to the same value as
|
|
6
|
+
// the array and the object literal; what a reader needs to know is that the ELEMENT cannot
|
|
7
|
+
// tell them apart — that `el.model = ['Blue', 'Teal']` and
|
|
8
|
+
// `el.model = new Gtk.StringList({ strings: ['Blue', 'Teal'] })` leave `<adw-combo-row>` in
|
|
9
|
+
// the same state, down to the `<option>` nodes it renders, and likewise for the two
|
|
10
|
+
// adjustment spellings on `<adw-spin-row>`. Four spellings, each asserted against what the
|
|
11
|
+
// element shows.
|
|
12
|
+
//
|
|
13
|
+
// The NativeScript port asserts the same four at its own widget door
|
|
14
|
+
// (`gtk-value-doors.spec.ts` there), so the two renderers cannot part on this without one
|
|
15
|
+
// of them failing.
|
|
16
|
+
//
|
|
17
|
+
// Compared as JSON rather than with `toEqual`: `@gjsify/unit`'s `toEqual` is `==`, so two
|
|
18
|
+
// different objects pass it whatever they hold.
|
|
19
|
+
|
|
20
|
+
import { describe, it, expect } from '@gjsify/unit';
|
|
21
|
+
|
|
22
|
+
import { GtkAdjustment, GtkStringList } from '@gjsify/adwaita-core';
|
|
23
|
+
|
|
24
|
+
import type { AdwComboRow } from './elements/adw-combo-row.js';
|
|
25
|
+
import type { AdwSpinRow } from './elements/adw-spin-row.js';
|
|
26
|
+
import type { GtkDropDown } from './elements/gtk-drop-down.js';
|
|
27
|
+
|
|
28
|
+
const COLOURS = ['Blue', 'Teal', 'Green'];
|
|
29
|
+
const FONT_SIZE = { lower: 0, upper: 100, value: 16, stepIncrement: 1 };
|
|
30
|
+
|
|
31
|
+
function mount<T extends HTMLElement>(tag: string): T {
|
|
32
|
+
const el = document.createElement(tag) as T;
|
|
33
|
+
document.body.appendChild(el);
|
|
34
|
+
return el;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Everything a `model` write leaves behind, the rendered options included. */
|
|
38
|
+
const modelState = (el: AdwComboRow | GtkDropDown, options: string) =>
|
|
39
|
+
JSON.stringify({ model: el.model, selected: el.selected, value: el.selectedValue, options });
|
|
40
|
+
|
|
41
|
+
/** The option labels the row actually paints, which is what a reader sees. */
|
|
42
|
+
const comboOptions = (el: HTMLElement) =>
|
|
43
|
+
[...el.querySelectorAll('option, .adw-drop-down-item-label')].map((node) => node.textContent).join('|');
|
|
44
|
+
|
|
45
|
+
export const GtkValueDoorsTest = async () => {
|
|
46
|
+
await describe('<adw-combo-row> takes a plain array and a Gtk.StringList alike', async () => {
|
|
47
|
+
await it('leaves the row in the same state, rendered options included', () => {
|
|
48
|
+
const array = mount<AdwComboRow>('adw-combo-row');
|
|
49
|
+
const list = mount<AdwComboRow>('adw-combo-row');
|
|
50
|
+
array.model = COLOURS;
|
|
51
|
+
list.model = new GtkStringList({ strings: COLOURS });
|
|
52
|
+
array.selected = 1;
|
|
53
|
+
list.selected = 1;
|
|
54
|
+
|
|
55
|
+
expect(modelState(list, comboOptions(list))).toBe(modelState(array, comboOptions(array)));
|
|
56
|
+
expect(list.selectedValue).toBe('Teal');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await it('follows a list that was appended to before it was assigned', () => {
|
|
60
|
+
const strings = new GtkStringList({ strings: ['Blue'] });
|
|
61
|
+
strings.append('Teal');
|
|
62
|
+
const row = mount<AdwComboRow>('adw-combo-row');
|
|
63
|
+
row.model = strings;
|
|
64
|
+
row.selected = 1;
|
|
65
|
+
|
|
66
|
+
expect(row.selectedValue).toBe('Teal');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await describe('<gtk-drop-down> takes a plain array and a Gtk.StringList alike', async () => {
|
|
71
|
+
await it('leaves the drop-down in the same state', () => {
|
|
72
|
+
const array = mount<GtkDropDown>('gtk-drop-down');
|
|
73
|
+
const list = mount<GtkDropDown>('gtk-drop-down');
|
|
74
|
+
array.model = COLOURS;
|
|
75
|
+
list.model = new GtkStringList({ strings: COLOURS });
|
|
76
|
+
array.selected = 2;
|
|
77
|
+
list.selected = 2;
|
|
78
|
+
|
|
79
|
+
expect(modelState(list, comboOptions(list))).toBe(modelState(array, comboOptions(array)));
|
|
80
|
+
expect(list.selectedValue).toBe('Green');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await describe('<adw-spin-row> takes a plain object and a Gtk.Adjustment alike', async () => {
|
|
85
|
+
await it('leaves the row in the same state', () => {
|
|
86
|
+
const literal = mount<AdwSpinRow>('adw-spin-row');
|
|
87
|
+
const adjustment = mount<AdwSpinRow>('adw-spin-row');
|
|
88
|
+
literal.adjustment = FONT_SIZE;
|
|
89
|
+
adjustment.adjustment = new GtkAdjustment(FONT_SIZE);
|
|
90
|
+
|
|
91
|
+
expect(JSON.stringify(adjustment.adjustment)).toBe(JSON.stringify(literal.adjustment));
|
|
92
|
+
expect(adjustment.value).toBe(literal.value);
|
|
93
|
+
expect(adjustment.value).toBe(16);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await it('steps by the increment the adjustment carries, whichever spelling set it', () => {
|
|
97
|
+
const row = mount<AdwSpinRow>('adw-spin-row');
|
|
98
|
+
row.adjustment = new GtkAdjustment({ ...FONT_SIZE, stepIncrement: 4 });
|
|
99
|
+
|
|
100
|
+
expect(row.adjustment.stepIncrement).toBe(4);
|
|
101
|
+
expect(row.value).toBe(16);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -57,6 +57,14 @@ export type { ApplyAccentOptions } from './accent.js';
|
|
|
57
57
|
export { AdwScrollShading } from './scroll-shading.js';
|
|
58
58
|
export type { AdwUndershootEdges } from './scroll-shading.js';
|
|
59
59
|
|
|
60
|
+
// The ADR 0051 tree instantiation half — turns an `@gjsify/adwaita-core/conformance`
|
|
61
|
+
// `SharedTreeNode` into real custom elements. Shipped (not test-only) so a consumer
|
|
62
|
+
// replaying an authored tree — a storybook fixture, a devtools probe, the shared-trees
|
|
63
|
+
// spec itself — has a route to it that is not a `.spec.ts` import; see that file's header
|
|
64
|
+
// for why `mountSharedTree` and not `buildSharedTree` alone is the instantiation half.
|
|
65
|
+
export { buildSharedTree, mountSharedTree } from './shared-tree-builder.js';
|
|
66
|
+
export type { MountedSharedTree } from './shared-tree-builder.js';
|
|
67
|
+
|
|
60
68
|
// The stylesheet compiles a chosen SUBSET of `@gjsify/adwaita-icons` (the whole set is
|
|
61
69
|
// ~1.07 MB of data-URI), so a name outside it draws the `image-missing` fallback. This is
|
|
62
70
|
// the way in for an app that needs a glyph this package does not ship — see
|
|
@@ -87,6 +95,11 @@ export { isIconAvailable, registerIcon } from './icon-registry.js';
|
|
|
87
95
|
export * as Adw from './namespace/adw.js';
|
|
88
96
|
export * as Gtk from './namespace/gtk.js';
|
|
89
97
|
|
|
98
|
+
// The THIRD namespace, and the one that defines no element: the GObject types an author
|
|
99
|
+
// CONSTRUCTS while building a tree. `Gio.Menu` is what makes `menuModel: menu` the same
|
|
100
|
+
// program here as on GJS — `./namespace/gio.ts` says what does not go in it.
|
|
101
|
+
export * as Gio from './namespace/gio.js';
|
|
102
|
+
|
|
90
103
|
// WHAT DID NOT MOVE INTO THE NAMESPACE, and the rule that decides it. A member exists
|
|
91
104
|
// for an element whose GIR tag names a real widget; `WEB_ELEMENT_ALIGNMENT` declares the
|
|
92
105
|
// rest `webOnly`, meaning no widget in the reference vocabulary stands behind it, so
|
|
@@ -127,7 +140,7 @@ export { AdwSidebarItem, AdwSidebarSection } from './elements/adw-sidebar.js';
|
|
|
127
140
|
|
|
128
141
|
// Slot wrappers and a declarative response: on GTK these are `set_content()`,
|
|
129
142
|
// `set_sheet()` and `add_response()` — calls, not widgets.
|
|
130
|
-
export { AdwBottomSheetContent, AdwBottomSheetSheet } from './elements/adw-bottom-sheet.js';
|
|
143
|
+
export { AdwBottomSheetBottomBar, AdwBottomSheetContent, AdwBottomSheetSheet } from './elements/adw-bottom-sheet.js';
|
|
131
144
|
export { AdwAlertResponse } from './elements/adw-alert-dialog.js';
|
|
132
145
|
|
|
133
146
|
// Supporting types — the option bags, enums and unions the widgets above take and
|
|
@@ -621,7 +621,8 @@ export const AdwKeyboardOperableTest = async () => {
|
|
|
621
621
|
//
|
|
622
622
|
// Prose cannot hold that: this line fails the commit that adds it.
|
|
623
623
|
// `<adw-inline-view-switcher>` has the same gap (status/open-todos.md).
|
|
624
|
-
|
|
624
|
+
// `active-name` joined the list without touching the axis: it picks a toggle.
|
|
625
|
+
expect([...AdwToggleGroup.observedAttributes]).toStrictEqual(['active', 'active-name', 'flat', 'round']);
|
|
625
626
|
});
|
|
626
627
|
|
|
627
628
|
await it('adw-toggle has no state a roving walk would have to skip', async () => {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// The GIO half of `@gjsify/adwaita-web`'s vocabulary — `Gio.Menu`, `Gio.MenuItem`.
|
|
2
|
+
// `export * as Gio from './namespace/gio.js'` in `src/index.ts` makes it the namespace,
|
|
3
|
+
// the same module shape `./adw.ts` and `./gtk.ts` take and for the same reason (one list
|
|
4
|
+
// carrying the value AND the type meaning — see the header of `./adw.ts`).
|
|
5
|
+
//
|
|
6
|
+
// RE-EXPORTED, NOT DEFINED. This package used to carry its own copy of the classes and
|
|
7
|
+
// said so; they hold no DOM, so they now live in `@gjsify/adwaita-core` beside the
|
|
8
|
+
// portable model they build. This barrel is the port's DOOR onto them, and
|
|
9
|
+
// `gio-menu.spec.ts` drives the whole suite through it.
|
|
10
|
+
//
|
|
11
|
+
// WHY THIS BARREL IS SHORT, AND WILL STAY SHORT. The other two hold ELEMENTS, and
|
|
12
|
+
// `check-vocabulary-alignment.mjs` derives their members from the elements this package
|
|
13
|
+
// defines. GIO defines none: what lands here is the handful of GObject types an Adwaita
|
|
14
|
+
// author CONSTRUCTS while building a tree — today the menu a `menuModel` property takes.
|
|
15
|
+
// `Gio.File`, `Gio.Settings`, `Gio.SimpleAction` and the rest of the library are NOT this
|
|
16
|
+
// package's business; a namespace that grows past what the elements consume is a second,
|
|
17
|
+
// unheld vocabulary.
|
|
18
|
+
|
|
19
|
+
export { GioMenu as Menu, GioMenuItem as MenuItem } from '@gjsify/adwaita-core';
|
package/src/namespace/gtk.ts
CHANGED
|
@@ -10,13 +10,27 @@
|
|
|
10
10
|
// declare. GTK4 has no radio type: a radio is a GtkCheckButton with its `group` set,
|
|
11
11
|
// which is what `<adw-radio>`'s own `why` in the ledger says. So the plain form takes the
|
|
12
12
|
// GIR name and the grouped one keeps its flat export in `src/index.ts`.
|
|
13
|
+
//
|
|
14
|
+
// TWO MEMBERS HERE ARE NOT WIDGETS — `Gtk.Adjustment` and `Gtk.StringList`, the values an
|
|
15
|
+
// `adjustment` and a `model` property take. ADR 0034 § Amendment 19 is what lets a
|
|
16
|
+
// clause-2 namespace carry them, `CONSTRUCTIBLE_VALUES` in `scripts/value-types.mjs` is
|
|
17
|
+
// where each one is declared, and the GIR answers for both in gtk-host's committed
|
|
18
|
+
// `generated/value-types.mts`. They are re-exported from `@gjsify/adwaita-core`, which is
|
|
19
|
+
// where the portable list and adjustment already live: the class IS that value wearing the
|
|
20
|
+
// GIR spelling, so `model: ['a','b']` and `model: new Gtk.StringList({ strings: ['a','b'] })`
|
|
21
|
+
// are the same write and nothing in this package had to learn a second input shape.
|
|
13
22
|
|
|
23
|
+
export { GtkAdjustment as Adjustment } from '@gjsify/adwaita-core';
|
|
24
|
+
export { GtkActionBar as ActionBar } from '../elements/gtk-action-bar.js';
|
|
25
|
+
export { GtkBox as Box } from '../elements/gtk-box.js';
|
|
14
26
|
export { GtkButton as Button } from '../elements/gtk-button.js';
|
|
15
27
|
export { GtkCheckButton as CheckButton } from '../elements/checks.js';
|
|
16
28
|
export { GtkDropDown as DropDown } from '../elements/gtk-drop-down.js';
|
|
17
29
|
export { GtkEntry as Entry } from '../elements/gtk-entry.js';
|
|
18
30
|
export { GtkImage as Image } from '../elements/gtk-image.js';
|
|
31
|
+
export { GtkLabel as Label } from '../elements/gtk-label.js';
|
|
19
32
|
export { GtkMenuButton as MenuButton } from '../elements/gtk-menu-button.js';
|
|
20
33
|
export { GtkPopover as Popover } from '../elements/gtk-popover.js';
|
|
21
34
|
export { GtkProgressBar as ProgressBar } from '../elements/gtk-progress-bar.js';
|
|
35
|
+
export { GtkStringList as StringList } from '@gjsify/adwaita-core';
|
|
22
36
|
export { GtkSwitch as Switch } from '../elements/gtk-switch.js';
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
// THE INSTANTIATION HALF OF ADR 0051, SHIPPED — the `adwaita-web` third of what #1726 did
|
|
2
|
+
// for `gtk-host` and #1729 did for `adwaita-nativescript`. Turning an
|
|
3
|
+
// `@gjsify/adwaita-core/conformance` `SharedTreeNode` into real `<adw-*>`/`<gtk-*>` custom
|
|
4
|
+
// elements is this renderer's OWN translation, not test code, and it lived only inside
|
|
5
|
+
// `shared-trees.spec.ts` — unreachable by a storybook fixture, a devtools probe replaying a
|
|
6
|
+
// gallery block, or a second suite, all of which would have had to import a `.spec.ts` file
|
|
7
|
+
// to reach it. `hostTagOf`/`attributeOf` moved to `@gjsify/adwaita-core/tags` for exactly
|
|
8
|
+
// this: a builder shipping FROM a package, not a dev-only driver reading `scripts/`, can
|
|
9
|
+
// depend on the two case rules without depending on `scripts/`, which cannot ship inside an
|
|
10
|
+
// npm package at all.
|
|
11
|
+
//
|
|
12
|
+
// TWO FUNCTIONS, NOT ONE WITH A FLAG — the split neither sibling builder needed.
|
|
13
|
+
// `gtk-host`'s `materialize`/`insert` and NativeScript's `_addChildFromBuilder` each fully
|
|
14
|
+
// REALISE a widget at construction; nothing later changes what it is. A Custom Element is
|
|
15
|
+
// not: most elements this package defines build their internals in `connectedCallback`
|
|
16
|
+
// (`shared-trees.spec.ts`'s own note: "these elements build on connect"), which runs only
|
|
17
|
+
// once the element is CONNECTED to a document. So `buildSharedTree` alone — `createElement`
|
|
18
|
+
// + `setAttribute`, recursing into children — is precisely the builder that ships dead
|
|
19
|
+
// nodes: it stays exported for a caller supplying its own attachment point (its result
|
|
20
|
+
// becomes live the moment ANYTHING connects it, same as a bare `document.createElement`
|
|
21
|
+
// always has), but the complete path most callers want is `mountSharedTree`: build, attach
|
|
22
|
+
// under a fresh host `<div>` in `document.body`, hand back the realised root.
|
|
23
|
+
//
|
|
24
|
+
// `unmount` IS PART OF THE SAME LIFECYCLE, NOT A TEST HOOK. `shared-trees.spec.ts`'s own
|
|
25
|
+
// `finally` block is test POLICY (never leave a red test's tree mounted for the next one to
|
|
26
|
+
// trip over); the ability to detach what was attached is a plain fact about anything with a
|
|
27
|
+
// `connectedCallback`, so `mountSharedTree` hands it back rather than making a caller reach
|
|
28
|
+
// into a host `<div>` it was never given.
|
|
29
|
+
//
|
|
30
|
+
// Placed flat under `src/`, at parity with this package's other single-purpose modules
|
|
31
|
+
// (`accent.ts`, `breakpoints.ts`, `icon-registry.ts`) rather than a new `conformance/` or
|
|
32
|
+
// `builder/` directory for one file: unlike `gtk-host`, this package has no existing
|
|
33
|
+
// `conformance/` home to add to. Exported through the existing barrel, `src/index.ts` — the
|
|
34
|
+
// package's `exports` map ships only `.`, and a new subpath would buy nothing for a module
|
|
35
|
+
// this small, most of which (the `SharedTreeNode` type) is erased at build anyway.
|
|
36
|
+
|
|
37
|
+
import type { SharedTreeNode } from '@gjsify/adwaita-core/conformance';
|
|
38
|
+
import { GTK_WIDGET_MARGIN_CSS, attributeOf, hostTagOf, propertyOf } from '@gjsify/adwaita-core/tags';
|
|
39
|
+
|
|
40
|
+
import { slottedChildrenOf } from './slotted-children.js';
|
|
41
|
+
|
|
42
|
+
/** One authored placement, kept so {@link mountSharedTree} can hold the renderer to it. */
|
|
43
|
+
interface PlacedChild {
|
|
44
|
+
parent: HTMLElement;
|
|
45
|
+
child: HTMLElement;
|
|
46
|
+
slot: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One node that authored `extensions`, kept so {@link mountSharedTree} can hold it too. */
|
|
50
|
+
interface ExtendedNode {
|
|
51
|
+
el: HTMLElement;
|
|
52
|
+
node: SharedTreeNode;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** What one build collects for the checks that can only run once the tree is connected. */
|
|
56
|
+
interface BuildRecord {
|
|
57
|
+
placed: PlacedChild[];
|
|
58
|
+
extended: ExtendedNode[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Whether `member` can be assigned on `el`: the nearest descriptor up the prototype chain is
|
|
63
|
+
* a writable data property or an accessor WITH a setter. `in` alone answers true for a
|
|
64
|
+
* getter-only accessor, whose assignment throws in strict code.
|
|
65
|
+
*/
|
|
66
|
+
function isWritable(el: object, member: string): boolean {
|
|
67
|
+
for (let at: object | null = el; at !== null; at = Object.getPrototypeOf(at) as object | null) {
|
|
68
|
+
const descriptor = Object.getOwnPropertyDescriptor(at, member);
|
|
69
|
+
if (descriptor === undefined) continue;
|
|
70
|
+
return descriptor.set !== undefined || descriptor.writable === true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A `SharedTreeNode`, realised as a DETACHED element tree: a tag, its authored properties as
|
|
77
|
+
* attributes, its style classes as classes, its extensions (ADR 0072) as the markup the element
|
|
78
|
+
* reads, its placement as `slot=`, its children, in that order — recursive and total,
|
|
79
|
+
* no tag list, no per-block case. A boolean authored property is the ATTRIBUTE'S PRESENCE
|
|
80
|
+
* (`toggleAttribute`), which is what every element in the corpus reads
|
|
81
|
+
* (`hasAttribute('revealed')`, `hasAttribute('expanded')`); spelling `"true"` would set a
|
|
82
|
+
* present attribute for `false` as well.
|
|
83
|
+
*
|
|
84
|
+
* EXCEPT AN AUTHORED `false` ON A PROPERTY THE ELEMENT DECLARES. Absence cannot say `false`
|
|
85
|
+
* where the GTK default is TRUE — `AdwNavigationPage:can-pop`, `GtkActionBar:revealed` —
|
|
86
|
+
* because those elements read an absent attribute as that default, so `can-pop: false`
|
|
87
|
+
* reached the page as `can-pop` unset and the page stayed poppable. The element's own
|
|
88
|
+
* property setter knows its attribute convention, so an authored `false` is written
|
|
89
|
+
* through it when the element (already upgraded: `createElement` of a defined tag
|
|
90
|
+
* constructs it) declares one; everything else keeps the presence rule. "Declares" means a
|
|
91
|
+
* member it can WRITE ({@link isWritable}): a getter-only accessor of the same name — the
|
|
92
|
+
* split button's and the menu button's read-only `active` — would throw a bare `TypeError`
|
|
93
|
+
* out of the assignment, so such a property falls back to the presence rule too.
|
|
94
|
+
*
|
|
95
|
+
* THE SLOT IS WRITTEN AS THE ATTRIBUTE THIS RENDERER ALREADY ROUTES ON, not translated:
|
|
96
|
+
* `src/slotted-children.ts` reads `slot=` off every light-DOM child and keeps the routing
|
|
97
|
+
* live. This builder read `tag`, `props` and `children` and dropped `slot` silently until a
|
|
98
|
+
* real `.blp` authored one — the `[top]` header bar landed in `adw-toolbar-view-content` and
|
|
99
|
+
* the window title was then discarded by `<adw-header-bar>`'s own build, at exit 0.
|
|
100
|
+
*
|
|
101
|
+
* NOT YET LIVE — see the file header. Nothing here has run `connectedCallback` until
|
|
102
|
+
* something connects it: {@link mountSharedTree} for the common case, or a caller's own
|
|
103
|
+
* container. A DETACHED build therefore cannot check a slot either: an element that has not
|
|
104
|
+
* upgraded has declared no slots yet, so the refusal below belongs to the mount.
|
|
105
|
+
*/
|
|
106
|
+
export function buildSharedTree(node: SharedTreeNode, record: BuildRecord = { placed: [], extended: [] }): HTMLElement {
|
|
107
|
+
const el = document.createElement(hostTagOf(node.tag));
|
|
108
|
+
// The id is how the TypeScript beside a `.blp` reaches this element
|
|
109
|
+
// (`root.querySelector('#…')`), the counterpart of `InternalChildren` on GTK.
|
|
110
|
+
if (node.id !== undefined) el.id = node.id;
|
|
111
|
+
for (const [prop, value] of Object.entries(node.props ?? {})) {
|
|
112
|
+
const member = propertyOf(prop);
|
|
113
|
+
if (value === false && isWritable(el, member)) (el as unknown as Record<string, unknown>)[member] = false;
|
|
114
|
+
else if (typeof value === 'boolean') el.toggleAttribute(attributeOf(prop), value);
|
|
115
|
+
else el.setAttribute(attributeOf(prop), String(value));
|
|
116
|
+
// A margin is also inline style (`GTK_WIDGET_MARGIN_CSS` says why); the attribute
|
|
117
|
+
// stays, since it is what the tree authored and what a reader of the DOM looks for.
|
|
118
|
+
const margin = GTK_WIDGET_MARGIN_CSS[attributeOf(prop)];
|
|
119
|
+
if (margin !== undefined) el.style.setProperty(margin, `${Number(value)}px`);
|
|
120
|
+
}
|
|
121
|
+
// `styleClasses` is `GtkWidget:css-classes`, and this renderer's door for it is the
|
|
122
|
+
// `class` attribute — what `.title-1`, `.dimmed` and `.card` select on. Unread, a
|
|
123
|
+
// `.blp`'s `styles ["title-1"]` reached the tree and never the page.
|
|
124
|
+
if (node.styleClasses !== undefined && node.styleClasses.length > 0) el.classList.add(...node.styleClasses);
|
|
125
|
+
writeExtensions(el, node);
|
|
126
|
+
if (node.extensions !== undefined) record.extended.push({ el, node });
|
|
127
|
+
for (const child of node.children ?? []) {
|
|
128
|
+
const childEl = buildSharedTree(child, record);
|
|
129
|
+
if (child.slot !== undefined) {
|
|
130
|
+
childEl.setAttribute('slot', child.slot);
|
|
131
|
+
record.placed.push({ parent: el, child: childEl, slot: child.slot });
|
|
132
|
+
}
|
|
133
|
+
el.append(childEl);
|
|
134
|
+
}
|
|
135
|
+
return el;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* ADR 0072's `extensions`, written in the markup this package already reads for each.
|
|
140
|
+
*
|
|
141
|
+
* `strings` is the `strings` attribute, a JSON array — `Gtk.StringList:strings` is a real
|
|
142
|
+
* property, and JSON is how `model` is written on the two elements that take the list (see
|
|
143
|
+
* `string-list-slot.ts`). `responses` are `<adw-alert-response>` children, the markup
|
|
144
|
+
* spelling of GtkBuilder's `<response>` that `<adw-alert-dialog>` consumes at connect. A
|
|
145
|
+
* translatable string is written as its source text: no renderer here translates, and the
|
|
146
|
+
* marking stays on the tree for whoever extracts it.
|
|
147
|
+
*/
|
|
148
|
+
function writeExtensions(el: HTMLElement, node: SharedTreeNode): void {
|
|
149
|
+
const strings = node.extensions?.strings;
|
|
150
|
+
if (strings !== undefined) el.setAttribute('strings', JSON.stringify(strings.map((string) => string.value)));
|
|
151
|
+
for (const response of node.extensions?.responses ?? []) {
|
|
152
|
+
const responseEl = document.createElement('adw-alert-response');
|
|
153
|
+
responseEl.id = response.id;
|
|
154
|
+
if (response.appearance !== undefined) responseEl.setAttribute('appearance', response.appearance);
|
|
155
|
+
if (response.enabled === false) responseEl.setAttribute('enabled', 'false');
|
|
156
|
+
responseEl.textContent = response.label;
|
|
157
|
+
el.append(responseEl);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The response API an element answers to — `<adw-alert-dialog>`'s, named after libadwaita's. */
|
|
162
|
+
interface ResponseReader {
|
|
163
|
+
getResponseLabel(id: string): string | null;
|
|
164
|
+
getResponseAppearance(id: string): string | null;
|
|
165
|
+
getResponseEnabled(id: string): boolean;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* An extension the realised element did not take is refused, after connect, like a slot.
|
|
170
|
+
*
|
|
171
|
+
* Both doors are markup the ELEMENT consumes, so writing them proves nothing: an element that
|
|
172
|
+
* has no `model` slot, or is not a dialog, leaves the list or the responses where the builder
|
|
173
|
+
* put them, and the widget renders empty at exit 0. So each is read back off the element. A
|
|
174
|
+
* string list must have been consumed (it is data and leaves the tree when taken); every
|
|
175
|
+
* response must be registered with the label, appearance and enabled state the tree authored.
|
|
176
|
+
*/
|
|
177
|
+
function refuseUnheldExtensions(extended: readonly ExtendedNode[]): void {
|
|
178
|
+
for (const { el, node } of extended) {
|
|
179
|
+
if (node.extensions?.strings !== undefined && el.isConnected) {
|
|
180
|
+
const parent = el.parentElement?.localName ?? 'nothing';
|
|
181
|
+
throw new Error(
|
|
182
|
+
`<${parent}> did not take the <${el.localName}> authored at "${el.getAttribute('slot') ?? ''}", so ` +
|
|
183
|
+
`its ${node.extensions.strings.length} string(s) reach no list.`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const reader = el as unknown as Partial<ResponseReader>;
|
|
187
|
+
for (const response of node.extensions?.responses ?? []) {
|
|
188
|
+
const held =
|
|
189
|
+
typeof reader.getResponseLabel === 'function' &&
|
|
190
|
+
reader.getResponseLabel(response.id) === response.label &&
|
|
191
|
+
reader.getResponseAppearance?.(response.id) === (response.appearance ?? 'default') &&
|
|
192
|
+
reader.getResponseEnabled?.(response.id) === (response.enabled ?? true);
|
|
193
|
+
if (!held) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`<${el.localName}> did not register the response "${response.id}" as authored, so the ` +
|
|
196
|
+
'dialog would show without it.',
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* A placement the element has no destination for is refused BY NAME, after connect.
|
|
205
|
+
*
|
|
206
|
+
* `bindSlottedChildren` copies the NATIVE assignment algorithm — an unmatched `slot=` name
|
|
207
|
+
* is assigned nowhere and the child visibly stays put — which is right for hand-written
|
|
208
|
+
* markup and is not a report. An authored tree is a claim about where a widget goes, so the
|
|
209
|
+
* builder that realises one has to say when the renderer could not honour it; a widget
|
|
210
|
+
* silently left beside its destination is the defect this whole path was measured on.
|
|
211
|
+
*
|
|
212
|
+
* The element's own `slots` declaration is the answer, never a list kept here: an element
|
|
213
|
+
* that binds a slot enrols itself in this refusal, and one that stops binding drops out of
|
|
214
|
+
* it visibly.
|
|
215
|
+
*
|
|
216
|
+
* AN ELEMENT THIS PACKAGE DOES NOT DEFINE IS NOT REFUSED, and that exemption is narrow on
|
|
217
|
+
* purpose. An undefined element has exactly ONE destination — itself — so a placement
|
|
218
|
+
* cannot land anywhere but where the tree authored it; what such a tree is really missing
|
|
219
|
+
* is the WIDGET, which is a wider gap than a slot and not this refusal's claim to make.
|
|
220
|
+
* (`AdwApplicationWindow` is the live case: a real `.blp` roots at one and this package has
|
|
221
|
+
* no element for it.) A DEFINED element that routes no named slot is refused like any
|
|
222
|
+
* other: it built a structure and chose not to route into it, so a name it does not have
|
|
223
|
+
* would leave the child beside that structure.
|
|
224
|
+
*/
|
|
225
|
+
function refuseUnknownSlots(placed: readonly PlacedChild[]): void {
|
|
226
|
+
for (const { parent, child, slot } of placed) {
|
|
227
|
+
if (customElements.get(parent.localName) === undefined) continue;
|
|
228
|
+
const binding = slottedChildrenOf(parent);
|
|
229
|
+
const known = (binding?.slots ?? []).map((declared) => declared.name).filter((name) => name !== undefined);
|
|
230
|
+
if (known.includes(slot)) continue;
|
|
231
|
+
throw new Error(
|
|
232
|
+
`<${parent.localName}> has no slot "${slot}", so the authored <${child.localName}> has nowhere ` +
|
|
233
|
+
`to go. Known slots: ${known.length > 0 ? known.join(', ') : 'none — it routes no named slot'}.`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** A tree {@link mountSharedTree} built and connected. */
|
|
239
|
+
export interface MountedSharedTree {
|
|
240
|
+
/** The authored root — connected, so every custom element under it has upgraded and run. */
|
|
241
|
+
root: HTMLElement;
|
|
242
|
+
/** Disconnects and discards the mount point. */
|
|
243
|
+
unmount: () => void;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* {@link buildSharedTree}, attached under a fresh host `<div>` in `document.body` so the tree
|
|
248
|
+
* — and every custom element in it — is REAL rather than merely constructed. This is the
|
|
249
|
+
* instantiation half a caller reading the corpus's elements normally wants; a bare
|
|
250
|
+
* `buildSharedTree` is for a caller that already has somewhere of its own to attach it.
|
|
251
|
+
*/
|
|
252
|
+
export function mountSharedTree(node: SharedTreeNode): MountedSharedTree {
|
|
253
|
+
const host = document.createElement('div');
|
|
254
|
+
const record: BuildRecord = { placed: [], extended: [] };
|
|
255
|
+
host.append(buildSharedTree(node, record));
|
|
256
|
+
document.body.append(host);
|
|
257
|
+
// After the append, because that is what upgrades the elements and runs the binds the
|
|
258
|
+
// refusal reads; before the return, because a caller handed a tree back has no way left
|
|
259
|
+
// to tell a placement that was honoured from one that was dropped.
|
|
260
|
+
try {
|
|
261
|
+
refuseUnknownSlots(record.placed);
|
|
262
|
+
refuseUnheldExtensions(record.extended);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
host.remove();
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
return { root: host.firstElementChild as HTMLElement, unmount: () => host.remove() };
|
|
268
|
+
}
|