@yschimke/livery-a2ui 0.0.0-stage → 0.11.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 (149) hide show
  1. package/README.md +194 -2
  2. package/dist/angular.d.ts +32 -0
  3. package/dist/angular.js +51 -0
  4. package/dist/api.d.ts +35 -0
  5. package/dist/api.js +35 -0
  6. package/dist/catalog.d.ts +18 -0
  7. package/dist/catalog.js +55 -0
  8. package/dist/cmp-player.d.ts +3 -0
  9. package/dist/cmp-player.js +125 -0
  10. package/dist/controller.d.ts +61 -0
  11. package/dist/controller.js +330 -0
  12. package/dist/index.d.ts +7 -0
  13. package/dist/index.js +4 -0
  14. package/dist/livery-web/livery-livery-web.d.mts +75 -0
  15. package/dist/livery-web/livery-livery-web.import-object.mjs +370 -0
  16. package/dist/livery-web/livery-livery-web.js-builtins.mjs +52 -0
  17. package/dist/livery-web/livery-livery-web.mjs +104 -0
  18. package/dist/livery-web/livery-livery-web.wasm +0 -0
  19. package/dist/livery-web-compose/fonts/NotoSansSymbols2-OFL.txt +117 -0
  20. package/dist/livery-web-compose/fonts/NotoSansSymbols2-Regular.ttf +0 -0
  21. package/dist/livery-web-compose/fonts/README.md +8 -0
  22. package/dist/livery-web-compose/fonts/RobotoFlex-OFL.txt +93 -0
  23. package/dist/livery-web-compose/fonts/RobotoFlex.ttf +0 -0
  24. package/dist/livery-web-compose/livery-livery-web-compose.d.mts +60 -0
  25. package/dist/livery-web-compose/livery-livery-web-compose.import-object.mjs +653 -0
  26. package/dist/livery-web-compose/livery-livery-web-compose.js-builtins.mjs +52 -0
  27. package/dist/livery-web-compose/livery-livery-web-compose.mjs +89 -0
  28. package/dist/livery-web-compose/livery-livery-web-compose.wasm +0 -0
  29. package/dist/livery-web-compose/skiko.mjs +1118 -0
  30. package/dist/livery-web-compose/skiko.wasm +0 -0
  31. package/dist/livery.d.ts +165 -0
  32. package/dist/livery.js +87 -0
  33. package/dist/player.d.ts +2 -0
  34. package/dist/player.js +2 -0
  35. package/dist/react.d.ts +24 -0
  36. package/dist/react.js +36 -0
  37. package/node_modules/@js-joda/core/CHANGELOG.md +363 -0
  38. package/node_modules/@js-joda/core/CheatSheet.md +947 -0
  39. package/node_modules/@js-joda/core/LICENSE +34 -0
  40. package/node_modules/@js-joda/core/README.md +155 -0
  41. package/node_modules/@js-joda/core/bower.json +21 -0
  42. package/node_modules/@js-joda/core/dist/js-joda.d.ts +2452 -0
  43. package/node_modules/@js-joda/core/dist/js-joda.esm.js +13564 -0
  44. package/node_modules/@js-joda/core/dist/js-joda.flow.js +949 -0
  45. package/node_modules/@js-joda/core/dist/js-joda.js +13630 -0
  46. package/node_modules/@js-joda/core/dist/js-joda.js.map +1 -0
  47. package/node_modules/@js-joda/core/dist/js-joda.min.js +5 -0
  48. package/node_modules/@js-joda/core/package.json +76 -0
  49. package/node_modules/@js-joda/core/src/Clock.js +330 -0
  50. package/node_modules/@js-joda/core/src/DayOfWeek.js +451 -0
  51. package/node_modules/@js-joda/core/src/Duration.js +1205 -0
  52. package/node_modules/@js-joda/core/src/Enum.js +30 -0
  53. package/node_modules/@js-joda/core/src/Instant.js +1012 -0
  54. package/node_modules/@js-joda/core/src/LocalDate.js +1703 -0
  55. package/node_modules/@js-joda/core/src/LocalDateTime.js +1686 -0
  56. package/node_modules/@js-joda/core/src/LocalTime.js +1415 -0
  57. package/node_modules/@js-joda/core/src/MathUtil.js +236 -0
  58. package/node_modules/@js-joda/core/src/Month.js +600 -0
  59. package/node_modules/@js-joda/core/src/MonthDay.js +730 -0
  60. package/node_modules/@js-joda/core/src/OffsetDateTime.js +757 -0
  61. package/node_modules/@js-joda/core/src/OffsetTime.js +621 -0
  62. package/node_modules/@js-joda/core/src/Period.js +933 -0
  63. package/node_modules/@js-joda/core/src/StringUtil.js +43 -0
  64. package/node_modules/@js-joda/core/src/Year.js +1015 -0
  65. package/node_modules/@js-joda/core/src/YearConstants.js +21 -0
  66. package/node_modules/@js-joda/core/src/YearMonth.js +1096 -0
  67. package/node_modules/@js-joda/core/src/ZoneId.js +236 -0
  68. package/node_modules/@js-joda/core/src/ZoneIdFactory.js +201 -0
  69. package/node_modules/@js-joda/core/src/ZoneOffset.js +486 -0
  70. package/node_modules/@js-joda/core/src/ZoneRegion.js +72 -0
  71. package/node_modules/@js-joda/core/src/ZonedDateTime.js +2007 -0
  72. package/node_modules/@js-joda/core/src/_init.js +68 -0
  73. package/node_modules/@js-joda/core/src/assert.js +33 -0
  74. package/node_modules/@js-joda/core/src/chrono/ChronoLocalDate.js +230 -0
  75. package/node_modules/@js-joda/core/src/chrono/ChronoLocalDateTime.js +130 -0
  76. package/node_modules/@js-joda/core/src/chrono/ChronoZonedDateTime.js +194 -0
  77. package/node_modules/@js-joda/core/src/chrono/IsoChronology.js +235 -0
  78. package/node_modules/@js-joda/core/src/convert.js +83 -0
  79. package/node_modules/@js-joda/core/src/errors.js +54 -0
  80. package/node_modules/@js-joda/core/src/format/DateTimeBuilder.js +535 -0
  81. package/node_modules/@js-joda/core/src/format/DateTimeFormatter.js +728 -0
  82. package/node_modules/@js-joda/core/src/format/DateTimeFormatterBuilder.js +1545 -0
  83. package/node_modules/@js-joda/core/src/format/DateTimeParseContext.js +297 -0
  84. package/node_modules/@js-joda/core/src/format/DateTimePrintContext.js +138 -0
  85. package/node_modules/@js-joda/core/src/format/DecimalStyle.js +113 -0
  86. package/node_modules/@js-joda/core/src/format/EnumMap.js +70 -0
  87. package/node_modules/@js-joda/core/src/format/ParsePosition.js +31 -0
  88. package/node_modules/@js-joda/core/src/format/ResolverStyle.js +93 -0
  89. package/node_modules/@js-joda/core/src/format/SignStyle.js +38 -0
  90. package/node_modules/@js-joda/core/src/format/StringBuilder.js +47 -0
  91. package/node_modules/@js-joda/core/src/format/TextStyle.js +114 -0
  92. package/node_modules/@js-joda/core/src/format/parser/CharLiteralPrinterParser.js +46 -0
  93. package/node_modules/@js-joda/core/src/format/parser/CompositePrinterParser.js +89 -0
  94. package/node_modules/@js-joda/core/src/format/parser/FractionPrinterParser.js +162 -0
  95. package/node_modules/@js-joda/core/src/format/parser/NumberPrinterParser.js +367 -0
  96. package/node_modules/@js-joda/core/src/format/parser/OffsetIdPrinterParser.js +177 -0
  97. package/node_modules/@js-joda/core/src/format/parser/PadPrinterParserDecorator.js +81 -0
  98. package/node_modules/@js-joda/core/src/format/parser/SettingsParser.js +44 -0
  99. package/node_modules/@js-joda/core/src/format/parser/StringLiteralPrinterParser.js +39 -0
  100. package/node_modules/@js-joda/core/src/format/parser/ZoneIdPrinterParser.js +218 -0
  101. package/node_modules/@js-joda/core/src/js-joda.js +210 -0
  102. package/node_modules/@js-joda/core/src/license-preamble.js +3 -0
  103. package/node_modules/@js-joda/core/src/temporal/ChronoField.js +470 -0
  104. package/node_modules/@js-joda/core/src/temporal/ChronoUnit.js +310 -0
  105. package/node_modules/@js-joda/core/src/temporal/DefaultInterfaceTemporal.js +118 -0
  106. package/node_modules/@js-joda/core/src/temporal/IsoFields.js +859 -0
  107. package/node_modules/@js-joda/core/src/temporal/NativeJsTemporal.js +105 -0
  108. package/node_modules/@js-joda/core/src/temporal/Temporal.js +346 -0
  109. package/node_modules/@js-joda/core/src/temporal/TemporalAccessor.js +125 -0
  110. package/node_modules/@js-joda/core/src/temporal/TemporalAdjuster.js +92 -0
  111. package/node_modules/@js-joda/core/src/temporal/TemporalAdjusters.js +464 -0
  112. package/node_modules/@js-joda/core/src/temporal/TemporalAmount.js +166 -0
  113. package/node_modules/@js-joda/core/src/temporal/TemporalField.js +248 -0
  114. package/node_modules/@js-joda/core/src/temporal/TemporalQueries.js +223 -0
  115. package/node_modules/@js-joda/core/src/temporal/TemporalQueriesFactory.js +77 -0
  116. package/node_modules/@js-joda/core/src/temporal/TemporalQuery.js +107 -0
  117. package/node_modules/@js-joda/core/src/temporal/TemporalUnit.js +179 -0
  118. package/node_modules/@js-joda/core/src/temporal/ValueRange.js +257 -0
  119. package/node_modules/@js-joda/core/src/use.js +20 -0
  120. package/node_modules/@js-joda/core/src/zone/SystemDefaultZoneId.js +31 -0
  121. package/node_modules/@js-joda/core/src/zone/SystemDefaultZoneRules.js +162 -0
  122. package/node_modules/@js-joda/core/src/zone/ZoneOffsetTransition.js +289 -0
  123. package/node_modules/@js-joda/core/src/zone/ZoneRules.js +464 -0
  124. package/node_modules/@js-joda/core/src/zone/ZoneRulesProvider.js +36 -0
  125. package/node_modules/@yschimke/livery-web/README.md +112 -0
  126. package/node_modules/@yschimke/livery-web/dist/descriptor.d.ts +24 -0
  127. package/node_modules/@yschimke/livery-web/dist/descriptor.js +32 -0
  128. package/node_modules/@yschimke/livery-web/dist/index.d.ts +9 -0
  129. package/node_modules/@yschimke/livery-web/dist/index.js +5 -0
  130. package/node_modules/@yschimke/livery-web/dist/livery-web/livery-livery-web-core.d.mts +63 -0
  131. package/node_modules/@yschimke/livery-web/dist/livery-web/livery-livery-web-core.import-object.mjs +333 -0
  132. package/node_modules/@yschimke/livery-web/dist/livery-web/livery-livery-web-core.js-builtins.mjs +52 -0
  133. package/node_modules/@yschimke/livery-web/dist/livery-web/livery-livery-web-core.mjs +92 -0
  134. package/node_modules/@yschimke/livery-web/dist/livery-web/livery-livery-web-core.wasm +0 -0
  135. package/node_modules/@yschimke/livery-web/dist/player.d.ts +65 -0
  136. package/node_modules/@yschimke/livery-web/dist/player.js +117 -0
  137. package/node_modules/@yschimke/livery-web/dist/preview.d.ts +57 -0
  138. package/node_modules/@yschimke/livery-web/dist/preview.js +214 -0
  139. package/node_modules/@yschimke/livery-web/dist/publication.d.ts +7 -0
  140. package/node_modules/@yschimke/livery-web/dist/publication.js +103 -0
  141. package/node_modules/@yschimke/livery-web/dist/session.d.ts +71 -0
  142. package/node_modules/@yschimke/livery-web/dist/session.js +55 -0
  143. package/node_modules/@yschimke/livery-web/dist/viewer.d.ts +1 -0
  144. package/node_modules/@yschimke/livery-web/dist/viewer.js +18 -0
  145. package/node_modules/@yschimke/livery-web/package.json +39 -0
  146. package/node_modules/@yschimke/livery-web/vendor/PROVENANCE.md +18 -0
  147. package/node_modules/@yschimke/livery-web/vendor/remote-compose-player.d.mts +30 -0
  148. package/node_modules/@yschimke/livery-web/vendor/remote-compose-player.mjs +86 -0
  149. package/package.json +102 -4
@@ -0,0 +1,117 @@
1
+ import { createHostApi, createPlayer } from '../vendor/remote-compose-player.mjs';
2
+ /**
3
+ * The TypeScript Remote Compose player on a canvas in [container], in the page itself. A document
4
+ * is loaded once; its named values are set on it while it plays, so a data change moves the values
5
+ * and never reloads the document (Remote Compose's model, as `LiveryTemplate` does on Compose).
6
+ */
7
+ export class RcCanvasPlayer {
8
+ constructor(container, onAction, options = {}) {
9
+ this.onAction = onAction;
10
+ this.options = options;
11
+ this.values = new Map();
12
+ /** Shows run one after another; one asked for while another runs replaces any still queued. */
13
+ this.queue = Promise.resolve();
14
+ this.latest = 0;
15
+ this.disposed = false;
16
+ this.epoch = 0;
17
+ /** Documents loaded and values set, for tests and debugging. */
18
+ this.stats = { loads: 0, sets: 0 };
19
+ this.handle = createPlayer(container, {});
20
+ this.handle.canvas.style.display = 'block';
21
+ this.host = this.bindHost();
22
+ }
23
+ bindHost() {
24
+ const host = createHostApi(this.handle.player);
25
+ host.onAction('*', (value, name) => this.onAction({ name: name.replace(/^USER:/, ''), valueType: valueTypeOf(value), value }));
26
+ return host;
27
+ }
28
+ /**
29
+ * Shows [documentKey]'s document with [values]: loads [document] only when the key differs from
30
+ * the one playing, then sets each value that changed. A load is asynchronous, so shows are
31
+ * queued: each waits for the one before it, and a show superseded while it waits does nothing
32
+ * (resolves `false`), so an older one can never land after a newer one.
33
+ */
34
+ show(documentKey, document, values, widthPx, heightPx, density) {
35
+ const mine = ++this.latest;
36
+ const run = this.queue.then(async () => {
37
+ if (this.disposed || mine !== this.latest)
38
+ return false;
39
+ await this.apply(documentKey, document, values, widthPx, heightPx, density);
40
+ return !this.disposed && mine === this.latest;
41
+ });
42
+ this.queue = run.catch(() => { });
43
+ return run;
44
+ }
45
+ async apply(documentKey, document, values, widthPx, heightPx, density) {
46
+ const geometry = `${widthPx}/${heightPx}/${density}`;
47
+ // Some documents retain stale click regions after reflow; hosts may opt into reloading.
48
+ if (documentKey !== this.key || (this.options.reloadOnResize && geometry !== this.geometry)) {
49
+ const epoch = this.epoch;
50
+ this.handle.player.setDensity(density);
51
+ // Sized before the load too: the player fixes its touch mapping from the canvas a document
52
+ // loads into, so a document loaded at the default 256 px missed taps past it.
53
+ this.resize(widthPx, heightPx, density);
54
+ await this.handle.loadFromArrayBuffer(document.slice().buffer);
55
+ if (this.disposed) {
56
+ this.handle.destroy();
57
+ return;
58
+ }
59
+ if (epoch !== this.epoch)
60
+ return;
61
+ // The vendor host observes document replacement on a 50ms timer. Bind a fresh host
62
+ // now, while the new document is present, so show() never loses an immediate tap.
63
+ this.host.dispose();
64
+ this.host = this.bindHost();
65
+ this.key = documentKey;
66
+ this.geometry = geometry;
67
+ this.values.clear();
68
+ this.stats.loads++;
69
+ this.resize(widthPx, heightPx, density);
70
+ }
71
+ // A document's variables are in Remote Compose's `USER:` domain; set each by the name it declares.
72
+ const declared = this.host.variables();
73
+ for (const [name, value] of Object.entries(values)) {
74
+ const text = JSON.stringify(value);
75
+ if (this.values.get(name) === text)
76
+ continue;
77
+ const as = `USER:${name}` in declared ? `USER:${name}` : name;
78
+ if (!(as in declared))
79
+ continue; // the document doesn't read it
80
+ this.host.set(as, value);
81
+ this.values.set(name, text);
82
+ this.stats.sets++;
83
+ }
84
+ this.handle.player.repaint();
85
+ const measured = this.handle.player.measureSize();
86
+ this.measuredHeightDp = measured ? measured.height / density : undefined;
87
+ }
88
+ /** The canvas at [widthPx] × [heightPx] CSS pixels. */
89
+ resize(widthPx, heightPx, density) {
90
+ if (!this.disposed && widthPx > 0 && heightPx > 0)
91
+ this.handle.resize(Math.round(widthPx * density), Math.round(heightPx * density));
92
+ }
93
+ /** Forgets the document playing, so the next [show] loads its document again. */
94
+ forget() {
95
+ this.latest++;
96
+ this.epoch++;
97
+ this.key = undefined;
98
+ this.geometry = undefined;
99
+ this.values.clear();
100
+ }
101
+ dispose() {
102
+ if (this.disposed)
103
+ return;
104
+ this.disposed = true;
105
+ this.latest++;
106
+ this.host.dispose();
107
+ this.handle.destroy();
108
+ void this.queue.then(() => this.handle.destroy(), () => this.handle.destroy());
109
+ }
110
+ }
111
+ function valueTypeOf(value) {
112
+ if (value === undefined || value === null)
113
+ return 'none';
114
+ if (typeof value === 'number')
115
+ return Number.isInteger(value) ? 'int' : 'float';
116
+ return 'string';
117
+ }
@@ -0,0 +1,57 @@
1
+ import { type NamedAction } from './player.js';
2
+ import { type WebConfig, type Routed } from './session.js';
3
+ export interface PreviewOptions {
4
+ /** Host-owned trust and capabilities. These cannot be supplied by the descriptor. */
5
+ config: Omit<WebConfig, 'catalog'>;
6
+ /** Minimal embeds retain theme/reset; full previews also expose width/examples. */
7
+ controls?: 'full' | 'minimal';
8
+ /** Developer diagnostics, hidden by public publication pages. */
9
+ showActionLog?: boolean;
10
+ fetch?: typeof fetch;
11
+ /** Host-provided example overrides; never application scripts. */
12
+ examples?: Record<string, Record<string, unknown>>;
13
+ }
14
+ /** Experimental fixture viewer. Each instance owns its data and its Wasm session. */
15
+ export declare class ComponentPreview {
16
+ readonly element: HTMLElement;
17
+ private options;
18
+ private session?;
19
+ private player?;
20
+ private publication?;
21
+ private render?;
22
+ private data;
23
+ private sequence;
24
+ private disposed;
25
+ private inView;
26
+ private theme;
27
+ private width;
28
+ private readonly abort;
29
+ private readonly observer;
30
+ private readonly resizeObserver;
31
+ private renderedWidth;
32
+ private readonly surface;
33
+ private readonly status;
34
+ private readonly log;
35
+ private readonly controls;
36
+ private readonly visibility;
37
+ readonly ready: Promise<void>;
38
+ readonly actions: Routed[];
39
+ constructor(element: HTMLElement, url: string, options: PreviewOptions);
40
+ get values(): Record<string, unknown>;
41
+ get stats(): {
42
+ loads: number;
43
+ sets: number;
44
+ };
45
+ get state(): string;
46
+ private open;
47
+ /** Takes example data; the core binder, not this UI, decides whether it fits. */
48
+ setData(data: Record<string, unknown>): Promise<void>;
49
+ reset(): Promise<void>;
50
+ dispatch(action: NamedAction): Promise<void>;
51
+ private update;
52
+ private suspend;
53
+ dispose(): void;
54
+ private fail;
55
+ private setStatus;
56
+ private addControls;
57
+ }
@@ -0,0 +1,214 @@
1
+ import { descriptor, previewUrl } from './descriptor.js';
2
+ import { RcCanvasPlayer } from './player.js';
3
+ import { WebSession } from './session.js';
4
+ const light = { primary: '#FF6750A4', onSurface: '#FF1D1B20', surface: '#FFF7F2FA' };
5
+ const dark = { primary: '#FFD0BCFF', onSurface: '#FFE6E1E5', surface: '#FF211F26' };
6
+ /** Experimental fixture viewer. Each instance owns its data and its Wasm session. */
7
+ export class ComponentPreview {
8
+ constructor(element, url, options) {
9
+ this.element = element;
10
+ this.options = options;
11
+ this.data = {};
12
+ this.sequence = 0;
13
+ this.disposed = false;
14
+ this.inView = true;
15
+ this.theme = light;
16
+ this.width = 360;
17
+ this.abort = new AbortController();
18
+ this.renderedWidth = 0;
19
+ this.surface = document.createElement('div');
20
+ this.status = document.createElement('p');
21
+ this.log = document.createElement('pre');
22
+ this.controls = document.createElement('div');
23
+ this.visibility = () => {
24
+ if (document.hidden || !this.inView)
25
+ this.suspend();
26
+ else
27
+ void this.update();
28
+ };
29
+ this.actions = [];
30
+ const link = document.createElement('a');
31
+ link.href = new URL(url, location.href).href;
32
+ link.textContent = 'Open preview';
33
+ const title = document.createElement('h2');
34
+ title.textContent = 'Component preview';
35
+ this.status.setAttribute('role', 'status');
36
+ this.log.setAttribute('role', 'log');
37
+ this.log.setAttribute('aria-label', 'Preview actions');
38
+ this.surface.setAttribute('aria-label', 'Interactive Remote Compose component');
39
+ this.controls.style.cssText = 'display:flex;gap:12px;flex-wrap:wrap;margin:12px 0';
40
+ this.surface.style.maxWidth = '100%';
41
+ this.element.replaceChildren(title, link, this.controls, this.surface, this.status);
42
+ if (this.options.showActionLog !== false)
43
+ this.element.append(this.log);
44
+ this.addControls();
45
+ document.addEventListener('visibilitychange', this.visibility);
46
+ this.observer = new IntersectionObserver(entries => { this.inView = entries[0]?.isIntersecting ?? false; this.visibility(); });
47
+ this.observer.observe(element);
48
+ this.resizeObserver = new ResizeObserver(() => { if (this.surface.clientWidth !== this.renderedWidth)
49
+ void this.update(); });
50
+ this.resizeObserver.observe(this.surface);
51
+ this.ready = this.open(new URL(url, location.href).href, title, link);
52
+ }
53
+ get values() { return { ...this.data }; }
54
+ get stats() { return this.player ? { ...this.player.stats } : { loads: 0, sets: 0 }; }
55
+ get state() { return this.element.dataset.state ?? 'loading'; }
56
+ async open(url, title, link) {
57
+ this.setStatus('loading', 'Loading preview…');
58
+ try {
59
+ previewUrl(url);
60
+ const response = await (this.options.fetch ?? fetch)(url, { signal: this.abort.signal, credentials: 'omit', redirect: 'error' });
61
+ if (!response.ok)
62
+ throw new Error(`Preview unavailable (${response.status})`);
63
+ const bytes = await response.arrayBuffer();
64
+ if (bytes.byteLength > 64 * 1024)
65
+ throw new Error('Preview descriptor is too large');
66
+ const text = new TextDecoder().decode(bytes);
67
+ const pub = descriptor(JSON.parse(text), url);
68
+ if (this.disposed)
69
+ return;
70
+ this.publication = pub;
71
+ this.data = structuredClone(pub.data);
72
+ title.textContent = pub.fallback.title;
73
+ link.href = pub.canonicalUrl;
74
+ const opened = await WebSession.open({ ...this.options.config, catalog: new URL('./', pub.catalog.manifestUrl).href }, this.abort.signal);
75
+ if (this.disposed) {
76
+ opened.close();
77
+ return;
78
+ }
79
+ this.session = opened;
80
+ await this.update();
81
+ }
82
+ catch (error) {
83
+ if (!this.disposed)
84
+ this.fail(error);
85
+ }
86
+ }
87
+ /** Takes example data; the core binder, not this UI, decides whether it fits. */
88
+ async setData(data) { this.data = structuredClone(data); await this.update(); }
89
+ async reset() { if (this.publication) {
90
+ this.actions.length = 0;
91
+ this.log.textContent = '';
92
+ const example = this.controls.querySelector('[aria-label="Example"]');
93
+ if (example)
94
+ example.value = 'Default';
95
+ await this.setData(this.publication.data);
96
+ } }
97
+ async dispatch(action) {
98
+ const active = this.render, session = this.session;
99
+ if (this.disposed || !active || !session)
100
+ return;
101
+ const result = await session.action(active.render, action.name, action.valueType, action.value);
102
+ if (this.disposed || this.render !== active)
103
+ return;
104
+ this.actions.push(result);
105
+ this.log.textContent = this.actions.slice(-8).map(a => JSON.stringify(a)).join('\n');
106
+ if (result.route === 'set') {
107
+ this.data = { ...this.data, [result.input]: result.value };
108
+ await this.update();
109
+ }
110
+ }
111
+ async update() {
112
+ const session = this.session, pub = this.publication;
113
+ if (!session || !pub || this.disposed || document.hidden || !this.inView)
114
+ return;
115
+ this.surface.style.width = `${this.width}px`;
116
+ const effectiveWidth = Math.min(this.width, this.surface.clientWidth || this.width);
117
+ this.renderedWidth = effectiveWidth;
118
+ const mine = ++this.sequence;
119
+ const previous = this.render;
120
+ try {
121
+ const space = { widthDp: effectiveWidth, density: devicePixelRatio || 1, theme: this.theme, colorScheme: this.theme === dark ? 'dark' : 'light', decisionId: `preview:${pub.publicationId}` };
122
+ let r = await session.render(pub.template.contract, this.data, space, pub.template.designId, pub.template.sha256);
123
+ if (!r.ok && r.waiting && mine === this.sequence) {
124
+ this.setStatus('loading', 'Loading component resources…');
125
+ await session.fetchImages(); // explicitly outside the cache-only render
126
+ if (mine !== this.sequence || this.disposed)
127
+ return;
128
+ r = await session.render(pub.template.contract, this.data, space, pub.template.designId, pub.template.sha256);
129
+ }
130
+ if (mine !== this.sequence || this.disposed) {
131
+ if (r.ok)
132
+ session.release(r.render);
133
+ return;
134
+ }
135
+ if (!r.ok) {
136
+ this.suspend();
137
+ this.setStatus('fallback', `${pub.fallback.description} (${r.reason})`);
138
+ return;
139
+ }
140
+ if (r.sha256 !== pub.template.sha256 || r.design !== pub.template.designId) {
141
+ session.release(r.render);
142
+ throw new Error('Pinned component unavailable');
143
+ }
144
+ this.render = r;
145
+ this.surface.style.width = `${this.width}px`;
146
+ this.surface.style.height = `${r.heightDp ?? 120}px`;
147
+ this.player ??= new RcCanvasPlayer(this.surface, a => { void this.dispatch(a).catch(e => this.fail(e)); }, { reloadOnResize: true });
148
+ const shown = await this.player.show(r.documentKey, r.document, r.values, effectiveWidth, r.heightDp ?? 120, devicePixelRatio || 1);
149
+ if (mine === this.sequence && !this.disposed && shown) {
150
+ if (!previous || previous.documentKey !== r.documentKey)
151
+ session.shown(r.render);
152
+ this.setStatus('ready', 'Interactive preview — changes stay in this preview.');
153
+ }
154
+ if (previous)
155
+ session.release(previous.render);
156
+ }
157
+ catch (error) {
158
+ if (previous)
159
+ session.release(previous.render);
160
+ if (mine === this.sequence && !this.disposed) {
161
+ this.suspend();
162
+ this.fail(error);
163
+ }
164
+ }
165
+ }
166
+ suspend() {
167
+ this.sequence++;
168
+ if (this.render)
169
+ this.session?.release(this.render.render);
170
+ this.render = undefined;
171
+ this.player?.dispose();
172
+ this.player = undefined;
173
+ this.surface.replaceChildren();
174
+ }
175
+ dispose() {
176
+ if (this.disposed)
177
+ return;
178
+ this.disposed = true;
179
+ this.abort.abort();
180
+ this.observer.disconnect();
181
+ this.resizeObserver.disconnect();
182
+ document.removeEventListener('visibilitychange', this.visibility);
183
+ this.suspend();
184
+ this.session?.close();
185
+ this.session = undefined;
186
+ this.controls.replaceChildren();
187
+ this.setStatus('disposed', 'Preview closed');
188
+ }
189
+ fail(error) { this.setStatus('fallback', `${this.publication?.fallback.description ?? 'Preview unavailable'} — ${error instanceof Error ? error.message : String(error)}`); }
190
+ setStatus(state, text) { this.element.dataset.state = state; this.status.textContent = text; }
191
+ addControls() {
192
+ const select = (label, values, change) => {
193
+ const wrapper = document.createElement('label');
194
+ wrapper.textContent = `${label} `;
195
+ const field = document.createElement('select');
196
+ field.setAttribute('aria-label', label);
197
+ for (const value of values)
198
+ field.add(new Option(value, value));
199
+ field.addEventListener('change', () => change(field.value));
200
+ wrapper.append(field);
201
+ this.controls.append(wrapper);
202
+ };
203
+ select('Theme', ['Light', 'Dark'], value => { this.theme = value === 'Dark' ? dark : light; void this.update(); });
204
+ if (this.options.controls !== 'minimal')
205
+ select('Width', ['360', '280', '480'], value => { this.width = Number(value); void this.update(); });
206
+ if (this.options.controls !== 'minimal' && Object.keys(this.options.examples ?? {}).length)
207
+ select('Example', ['Default', ...Object.keys(this.options.examples ?? {})], value => { if (this.publication)
208
+ void this.setData({ ...this.publication.data, ...this.options.examples?.[value] }); });
209
+ const reset = document.createElement('button');
210
+ reset.textContent = 'Reset';
211
+ reset.addEventListener('click', () => { void this.reset(); });
212
+ this.controls.append(reset);
213
+ }
214
+ }
@@ -0,0 +1,7 @@
1
+ import { ComponentPreview } from './preview.js';
2
+ export interface PublicationViewer {
3
+ preview: ComponentPreview;
4
+ dispose(): void;
5
+ }
6
+ /** Enhances a server-rendered publication. Configuration belongs to the host, not the publication. */
7
+ export declare function mountPublication(element: HTMLElement): PublicationViewer;
@@ -0,0 +1,103 @@
1
+ import { ComponentPreview } from './preview.js';
2
+ import { previewUrl } from './descriptor.js';
3
+ const mounted = new WeakMap();
4
+ /** Enhances a server-rendered publication. Configuration belongs to the host, not the publication. */
5
+ export function mountPublication(element) {
6
+ const existing = mounted.get(element);
7
+ if (existing)
8
+ return existing;
9
+ const localUrl = (value, label) => {
10
+ if (!value)
11
+ throw new Error(`Missing ${label}`);
12
+ const url = previewUrl(new URL(value, document.baseURI).href);
13
+ if (url.origin !== location.origin)
14
+ throw new Error(`${label} must use the host origin`);
15
+ return url.href;
16
+ };
17
+ const source = localUrl(element.dataset.descriptor ?? document.querySelector('link[rel="alternate"][type="application/vnd.livery.preview+json"]')?.href, 'preview descriptor');
18
+ const canonical = localUrl(element.dataset.canonical ?? document.querySelector('link[rel="canonical"]')?.href, 'canonical URL');
19
+ const config = JSON.parse(element.dataset.config ?? '{}');
20
+ if (!config || typeof config !== 'object' || Array.isArray(config))
21
+ throw new Error('Invalid host configuration');
22
+ const mode = element.dataset.mode ?? 'page';
23
+ if (mode !== 'page' && mode !== 'embed')
24
+ throw new Error('Unsupported viewer mode');
25
+ const body = document.createElement('div');
26
+ body.className = 'livery-publication-component';
27
+ element.classList.add('livery-publication');
28
+ element.replaceChildren(body);
29
+ const preview = new ComponentPreview(body, source, { config, controls: mode === 'embed' ? 'minimal' : 'full', showActionLog: false });
30
+ const share = document.createElement('nav');
31
+ share.className = 'livery-publication-share';
32
+ share.setAttribute('aria-label', mode === 'embed' ? 'Full preview' : 'Share preview');
33
+ const status = document.createElement('p');
34
+ status.className = 'livery-share-status';
35
+ status.setAttribute('role', 'status');
36
+ let disposed = false;
37
+ const abort = new AbortController();
38
+ const say = (text) => { if (!disposed)
39
+ status.textContent = text; };
40
+ if (mode === 'embed') {
41
+ const open = document.createElement('a');
42
+ open.textContent = 'Open full preview';
43
+ open.href = canonical;
44
+ open.target = '_blank';
45
+ open.rel = 'noopener';
46
+ share.append(open);
47
+ }
48
+ else {
49
+ const bluesky = document.createElement('a');
50
+ bluesky.textContent = 'Share to Bluesky';
51
+ bluesky.href = `https://bsky.app/intent/compose?${new URLSearchParams({ text: canonical })}`;
52
+ bluesky.target = '_blank';
53
+ bluesky.rel = 'noopener noreferrer';
54
+ share.append(bluesky);
55
+ if (typeof navigator.share === 'function') {
56
+ const system = document.createElement('button');
57
+ system.type = 'button';
58
+ system.textContent = 'Share…';
59
+ system.addEventListener('click', () => {
60
+ // Invoke inside this click, before any await: transient activation must reach the OS sheet.
61
+ void navigator.share({ title: document.title, url: canonical }).then(() => say('Shared'), error => {
62
+ say(error instanceof DOMException && error.name === 'AbortError' ? 'Share cancelled' : 'Sharing unavailable. Use Copy link instead.');
63
+ });
64
+ }, { signal: abort.signal });
65
+ share.append(system);
66
+ }
67
+ const copy = document.createElement('button');
68
+ copy.type = 'button';
69
+ copy.textContent = 'Copy link';
70
+ const manual = document.createElement('input');
71
+ manual.readOnly = true;
72
+ manual.value = canonical;
73
+ manual.hidden = true;
74
+ manual.setAttribute('aria-label', 'Preview link');
75
+ copy.addEventListener('click', () => {
76
+ const failed = () => { if (!disposed) {
77
+ manual.hidden = false;
78
+ manual.focus();
79
+ manual.select();
80
+ say('Select and copy the preview link.');
81
+ } };
82
+ if (!navigator.clipboard?.writeText) {
83
+ failed();
84
+ return;
85
+ }
86
+ void navigator.clipboard.writeText(canonical).then(() => say('Link copied'), failed);
87
+ }, { signal: abort.signal });
88
+ share.append(copy, manual);
89
+ }
90
+ element.append(share, status);
91
+ const viewer = { preview, dispose() {
92
+ if (disposed)
93
+ return;
94
+ disposed = true;
95
+ abort.abort();
96
+ preview.dispose();
97
+ share.remove();
98
+ status.remove();
99
+ mounted.delete(element);
100
+ } };
101
+ mounted.set(element, viewer);
102
+ return viewer;
103
+ }
@@ -0,0 +1,71 @@
1
+ export interface WebConfig {
2
+ catalog: string;
3
+ keys?: {
4
+ keyId: string;
5
+ publicKey: string;
6
+ contracts?: string[];
7
+ }[];
8
+ /** Development only. A descriptor cannot opt its own host into this policy. */
9
+ unsigned?: boolean;
10
+ capabilities?: {
11
+ surface: string;
12
+ maxRcApiLevel: number;
13
+ profiles: string[];
14
+ features?: string[];
15
+ maxBytes?: number;
16
+ };
17
+ imageHosts?: string[];
18
+ urlAllowlist?: string[];
19
+ appKinds?: string[];
20
+ }
21
+ export interface Space {
22
+ colorScheme?: string;
23
+ widthDp?: number;
24
+ heightDp?: number;
25
+ density?: number;
26
+ theme?: Record<string, string>;
27
+ decisionId?: string;
28
+ }
29
+ export type Ready = {
30
+ ok: true;
31
+ render: number;
32
+ document: Uint8Array;
33
+ documentKey: string;
34
+ values: Record<string, string | number>;
35
+ design: string;
36
+ sha256: string;
37
+ heightDp?: number;
38
+ };
39
+ export type Rendered = Ready | {
40
+ ok: false;
41
+ waiting?: boolean;
42
+ reason: string;
43
+ };
44
+ export type Routed = {
45
+ route: 'set';
46
+ input: string;
47
+ value: unknown;
48
+ } | {
49
+ route: 'app';
50
+ kind: string;
51
+ action: string;
52
+ args: Record<string, string>;
53
+ } | {
54
+ route: 'dropped';
55
+ reason: string;
56
+ };
57
+ /** Typed transport only: selection, binding and action validation happen in Kotlin. */
58
+ export declare class WebSession {
59
+ private lib;
60
+ readonly id: number;
61
+ private closed;
62
+ private constructor();
63
+ static open(config: WebConfig, signal?: AbortSignal): Promise<WebSession>;
64
+ render(contract: string, data: Record<string, unknown>, space: Space, design?: string, sha?: string): Promise<Rendered>;
65
+ action(render: number, name: string, type: string, value: unknown): Promise<Routed>;
66
+ fetchImages(): Promise<boolean>;
67
+ shown(render: number): void;
68
+ release(render: number): void;
69
+ close(): void;
70
+ private check;
71
+ }
@@ -0,0 +1,55 @@
1
+ let loading;
2
+ function module() {
3
+ const url = new URL('./livery-web/livery-livery-web-core.mjs', import.meta.url).href;
4
+ return loading ??= import(/* @vite-ignore */ url);
5
+ }
6
+ /** Typed transport only: selection, binding and action validation happen in Kotlin. */
7
+ export class WebSession {
8
+ constructor(lib, id) {
9
+ this.lib = lib;
10
+ this.id = id;
11
+ this.closed = false;
12
+ }
13
+ static async open(config, signal) {
14
+ const lib = await module();
15
+ signal?.throwIfAborted();
16
+ const id = lib.previewCreate(JSON.stringify(config));
17
+ const close = () => lib.previewClose(id);
18
+ signal?.addEventListener('abort', close, { once: true });
19
+ try {
20
+ await lib.previewConnect(id);
21
+ signal?.throwIfAborted();
22
+ return new WebSession(lib, id);
23
+ }
24
+ catch (error) {
25
+ close();
26
+ throw error;
27
+ }
28
+ finally {
29
+ signal?.removeEventListener('abort', close);
30
+ }
31
+ }
32
+ async render(contract, data, space, design, sha) {
33
+ this.check();
34
+ const r = JSON.parse(String(await this.lib.previewRender(this.id, contract, JSON.stringify(data), JSON.stringify(space), design ?? null, sha ?? null)));
35
+ if (r.ok)
36
+ r.document = Uint8Array.from(atob(r.document), c => c.charCodeAt(0));
37
+ return r;
38
+ }
39
+ async action(render, name, type, value) {
40
+ if (this.closed)
41
+ return { route: 'dropped', reason: 'preview disposed' };
42
+ return JSON.parse(String(await this.lib.previewAction(this.id, render, name, type, value == null ? null : String(value))));
43
+ }
44
+ async fetchImages() { this.check(); return String(await this.lib.previewFetchImages(this.id)) === 'true'; }
45
+ shown(render) { if (!this.closed)
46
+ this.lib.previewShown(this.id, render); }
47
+ release(render) { if (!this.closed)
48
+ this.lib.previewRelease(this.id, render); }
49
+ close() { if (!this.closed) {
50
+ this.closed = true;
51
+ this.lib.previewClose(this.id);
52
+ } }
53
+ check() { if (this.closed)
54
+ throw new Error('preview disposed'); }
55
+ }
@@ -0,0 +1 @@
1
+ export { mountPublication } from './publication.js';
@@ -0,0 +1,18 @@
1
+ import { mountPublication } from './publication.js';
2
+ const viewers = [];
3
+ for (const element of document.querySelectorAll('[data-livery-preview]')) {
4
+ try {
5
+ viewers.push(mountPublication(element));
6
+ }
7
+ catch (error) {
8
+ element.dataset.state = 'fallback';
9
+ const message = document.createElement('p');
10
+ message.setAttribute('role', 'status');
11
+ message.textContent = `Interactive preview unavailable — ${error instanceof Error ? error.message : String(error)}`;
12
+ element.replaceChildren(message);
13
+ }
14
+ }
15
+ // Browsers retain the document in the back/forward cache; its visibility handler pauses playback.
16
+ window.addEventListener('pagehide', event => { if (!event.persisted)
17
+ viewers.forEach(viewer => viewer.dispose()); });
18
+ export { mountPublication } from './publication.js';