@adia-ai/web-modules 0.8.36 → 0.8.38

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 (90) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +4 -4
  3. package/agent-admin/admin-roster/admin-roster.js +1 -1
  4. package/agent-admin/admin-settings/admin-settings.js +1 -1
  5. package/agent-admin/agent-admin/agent-admin.js +1 -1
  6. package/billing/billing-overview/billing-overview.a2ui.json +281 -0
  7. package/billing/billing-overview/billing-overview.class.js +838 -0
  8. package/billing/billing-overview/billing-overview.css +223 -0
  9. package/billing/billing-overview/billing-overview.js +16 -0
  10. package/billing/billing-overview/billing-overview.yaml +441 -0
  11. package/billing/index.js +21 -0
  12. package/billing/invoice-detail/invoice-detail.a2ui.json +276 -0
  13. package/billing/invoice-detail/invoice-detail.class.js +711 -0
  14. package/billing/invoice-detail/invoice-detail.css +305 -0
  15. package/billing/invoice-detail/invoice-detail.js +16 -0
  16. package/billing/invoice-detail/invoice-detail.yaml +411 -0
  17. package/billing/invoice-history/invoice-history.a2ui.json +315 -0
  18. package/billing/invoice-history/invoice-history.class.js +672 -0
  19. package/billing/invoice-history/invoice-history.css +130 -0
  20. package/billing/invoice-history/invoice-history.js +16 -0
  21. package/billing/invoice-history/invoice-history.yaml +398 -0
  22. package/billing/payment-method-form/payment-method-form.a2ui.json +310 -0
  23. package/billing/payment-method-form/payment-method-form.class.js +886 -0
  24. package/billing/payment-method-form/payment-method-form.css +149 -0
  25. package/billing/payment-method-form/payment-method-form.js +16 -0
  26. package/billing/payment-method-form/payment-method-form.yaml +430 -0
  27. package/billing/payment-method-list/payment-method-list.a2ui.json +283 -0
  28. package/billing/payment-method-list/payment-method-list.class.js +705 -0
  29. package/billing/payment-method-list/payment-method-list.css +128 -0
  30. package/billing/payment-method-list/payment-method-list.js +16 -0
  31. package/billing/payment-method-list/payment-method-list.yaml +355 -0
  32. package/billing/plan-picker/plan-picker.a2ui.json +349 -0
  33. package/billing/plan-picker/plan-picker.class.js +635 -0
  34. package/billing/plan-picker/plan-picker.css +227 -0
  35. package/billing/plan-picker/plan-picker.js +16 -0
  36. package/billing/plan-picker/plan-picker.yaml +453 -0
  37. package/chat/README.md +2 -2
  38. package/chat/chat-composer/chat-composer.js +2 -2
  39. package/chat/chat-empty/chat-empty.js +2 -2
  40. package/chat/chat-shell/chat-shell.js +6 -6
  41. package/chat/chat-sidebar/chat-sidebar.js +2 -2
  42. package/chat/chat-surfaces/chat-surfaces.js +3 -3
  43. package/chat/chat-thread/chat-thread.js +2 -2
  44. package/dashboard/dashboard-layout/dashboard-layout.a2ui.json +254 -0
  45. package/dashboard/dashboard-layout/dashboard-layout.css +190 -0
  46. package/dashboard/dashboard-layout/dashboard-layout.js +289 -0
  47. package/dashboard/dashboard-layout/dashboard-layout.yaml +374 -0
  48. package/dashboard/date-range-selector/date-range-selector.a2ui.json +236 -0
  49. package/dashboard/date-range-selector/date-range-selector.css +84 -0
  50. package/dashboard/date-range-selector/date-range-selector.js +686 -0
  51. package/dashboard/date-range-selector/date-range-selector.yaml +358 -0
  52. package/dashboard/index.js +14 -0
  53. package/dist/chat/chat-shell.min.js +1 -1
  54. package/dist/everything.min.js +121 -102
  55. package/editor/editor-canvas/editor-canvas.js +2 -2
  56. package/editor/editor-shell/editor-shell.js +2 -2
  57. package/editor/editor-sidebar/editor-sidebar.js +2 -2
  58. package/editor/editor-toolbar/editor-toolbar.js +2 -2
  59. package/feedback/confirm-dialog/confirm-dialog.a2ui.json +206 -0
  60. package/feedback/confirm-dialog/confirm-dialog.css +157 -0
  61. package/feedback/confirm-dialog/confirm-dialog.js +383 -0
  62. package/feedback/confirm-dialog/confirm-dialog.yaml +297 -0
  63. package/feedback/index.js +10 -0
  64. package/form/form-popover/form-popover.js +1 -1
  65. package/generative/index.d.ts +1 -1
  66. package/generative/index.js +2 -2
  67. package/onboarding/index.js +1 -0
  68. package/onboarding/onboarding-checklist/onboarding-checklist.a2ui.json +263 -0
  69. package/onboarding/onboarding-checklist/onboarding-checklist.css +157 -0
  70. package/onboarding/onboarding-checklist/onboarding-checklist.js +428 -0
  71. package/onboarding/onboarding-checklist/onboarding-checklist.yaml +236 -0
  72. package/package.json +99 -6
  73. package/runtime/a2ui-root/a2ui-root.js +3 -3
  74. package/runtime/gen-root/gen-root.js +5 -5
  75. package/settings/index.js +12 -0
  76. package/settings/integrations-page/integrations-page.a2ui.json +274 -0
  77. package/settings/integrations-page/integrations-page.css +177 -0
  78. package/settings/integrations-page/integrations-page.d.ts +77 -0
  79. package/settings/integrations-page/integrations-page.js +413 -0
  80. package/settings/integrations-page/integrations-page.yaml +317 -0
  81. package/settings/notification-preferences/notification-preferences.a2ui.json +245 -0
  82. package/settings/notification-preferences/notification-preferences.css +204 -0
  83. package/settings/notification-preferences/notification-preferences.js +622 -0
  84. package/settings/notification-preferences/notification-preferences.yaml +358 -0
  85. package/shell/admin-command/admin-command.js +2 -2
  86. package/shell/admin-shell/admin-shell.js +2 -2
  87. package/shell/admin-sidebar/admin-sidebar.js +2 -2
  88. package/shell/embed-shell/embed-shell.js +3 -3
  89. package/simple/simple-shell/simple-shell.js +1 -1
  90. package/theme/theme-panel/theme-panel.js +2 -2
@@ -0,0 +1,77 @@
1
+ /**
2
+ * `<integrations-page-ui>` — Module-tier Settings page composite — a
3
+ * searchable, category-grouped grid of <integration-card-ui> tiles
4
+ * (SPEC-062).
5
+ *
6
+ * @see https://ui-kit.exe.xyz/site/components/integrations-page
7
+ *
8
+ * Hand-authored alongside `<name>.yaml`; regenerate when the props /
9
+ * events shape changes.
10
+ */
11
+
12
+ import { UIElement } from '../../../web-components/core/element.js';
13
+
14
+ export interface IntegrationDescriptor {
15
+ /** Provider key — `slack`, `github`, etc. Required. */
16
+ provider: string;
17
+ /** Display name shown as the card title. Required. */
18
+ name: string;
19
+ /** Logo URL or registered icon name. Optional. */
20
+ logo?: string;
21
+ /** One-line description. Optional. */
22
+ description?: string;
23
+ /** Connection state. Required for status-driven button + badge. */
24
+ status: 'available' | 'connected' | 'error' | 'pending' | 'coming-soon';
25
+ /** Category bucket — used when `group-by="category"`. Optional. */
26
+ category?: string;
27
+ /** Error message shown when `status="error"`. Optional. */
28
+ 'error-message'?: string;
29
+ /** camelCase alias for `error-message`. Optional. */
30
+ errorMessage?: string;
31
+ }
32
+
33
+ export interface IntegrationsPageSearchEventDetail {
34
+ /** Lower-cased, trimmed query value. */
35
+ query: string;
36
+ }
37
+ export type IntegrationsPageSearchEvent =
38
+ CustomEvent<IntegrationsPageSearchEventDetail>;
39
+
40
+ export interface IntegrationsPageFilterChangeEventDetail {
41
+ /** The newly-selected status filter (`all` resets). */
42
+ status: 'all' | 'available' | 'connected' | 'error' | 'coming-soon';
43
+ }
44
+ export type IntegrationsPageFilterChangeEvent =
45
+ CustomEvent<IntegrationsPageFilterChangeEventDetail>;
46
+
47
+ export interface IntegrationsPageCardEventDetail {
48
+ /** Provider key of the card that fired. */
49
+ provider: string;
50
+ }
51
+ export type IntegrationsPageCardEvent =
52
+ CustomEvent<IntegrationsPageCardEventDetail>;
53
+
54
+ export class UIIntegrationsPage extends UIElement {
55
+ /** Array of integration descriptors. Each item satisfies SPEC-062. */
56
+ integrations: IntegrationDescriptor[];
57
+ /** Renders skeleton grid when true. */
58
+ loading: boolean;
59
+ /** Placeholder copy for the built-in search input. */
60
+ 'search-placeholder': string;
61
+ /** How to bucket cards into sections. */
62
+ 'group-by': 'category' | 'status' | 'none';
63
+ /** Restrict visible cards to a single status. `all` resets. */
64
+ 'status-filter': 'all' | 'available' | 'connected' | 'error' | 'coming-soon';
65
+
66
+ addEventListener<K extends keyof HTMLElementEventMap>(
67
+ type: K,
68
+ listener: (this: UIIntegrationsPage, ev: HTMLElementEventMap[K]) => unknown,
69
+ options?: boolean | AddEventListenerOptions,
70
+ ): void;
71
+ addEventListener(type: 'search', listener: (ev: IntegrationsPageSearchEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
72
+ addEventListener(type: 'filter-change', listener: (ev: IntegrationsPageFilterChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
73
+ addEventListener(type: 'connect', listener: (ev: IntegrationsPageCardEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
74
+ addEventListener(type: 'configure', listener: (ev: IntegrationsPageCardEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
75
+ addEventListener(type: 'disconnect', listener: (ev: IntegrationsPageCardEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
76
+ addEventListener(type: 'retry', listener: (ev: IntegrationsPageCardEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
77
+ }
@@ -0,0 +1,413 @@
1
+ /**
2
+ * <integrations-page-ui> — module-tier Settings composite.
3
+ *
4
+ * <integrations-page-ui id="settings-integrations" group-by="category">
5
+ * </integrations-page-ui>
6
+ * <script>
7
+ * document.querySelector('#settings-integrations').integrations = [
8
+ * { provider: 'slack', name: 'Slack', logo: '/slack.svg',
9
+ * status: 'connected', category: 'Communication',
10
+ * description: 'Send replies to channels.' },
11
+ * { provider: 'github', name: 'GitHub', logo: '/github.svg',
12
+ * status: 'available', category: 'Developer Tools',
13
+ * description: 'Sync issues + PRs.' },
14
+ * ];
15
+ * </script>
16
+ *
17
+ * Composes <integration-card-ui> × N inside <grid-ui> with a built-in
18
+ * <search-ui> filter + <segmented-ui> status-filter top bar. Owns the
19
+ * data (integrations prop) → renders cards; card-level events bubble
20
+ * up unchanged via <integration-card-ui>'s typed emits.
21
+ *
22
+ * Per ADR-0027 — composites that programmatically create primitives in
23
+ * their render() do NOT auto-import those primitives. Consumer (or
24
+ * demo shell) must explicitly import <integration-card-ui>,
25
+ * <grid-ui>, <search-ui>, <segmented-ui> + <segment-ui>, and
26
+ * <text-ui> in addition to this module.
27
+ */
28
+
29
+ import { UIElement, html } from '@adia-ai/web-components/core/element';
30
+
31
+ // Status filter top-bar options. Order matches the spec's intent:
32
+ // `all` first (default reset), then connection-state buckets.
33
+ const FILTER_OPTIONS = Object.freeze([
34
+ { value: 'all', label: 'All' },
35
+ { value: 'available', label: 'Available' },
36
+ { value: 'connected', label: 'Connected' },
37
+ { value: 'error', label: 'Issues' },
38
+ { value: 'coming-soon', label: 'Coming soon' },
39
+ ]);
40
+
41
+ const SKELETON_COUNT = 6;
42
+
43
+ class UIIntegrationsPage extends UIElement {
44
+ static properties = {
45
+ integrations: { type: Array, default: [] },
46
+ loading: { type: Boolean, default: false, reflect: true },
47
+ 'search-placeholder':{ type: String, default: 'Search integrations', reflect: true },
48
+ 'group-by': { type: String, default: 'category', reflect: true },
49
+ 'status-filter': { type: String, default: 'all', reflect: true },
50
+ };
51
+
52
+ static template = (host) => host._renderTemplate();
53
+
54
+ // ── lifecycle state ─────────────────────────────────────────────
55
+
56
+ #query = '';
57
+ #headerId = `intpage-h-${Math.random().toString(36).slice(2, 8)}`;
58
+ #searchTimer = null;
59
+
60
+ // ── attribute parsing ──────────────────────────────────────────
61
+
62
+ /**
63
+ * `integrations` accepts JSON via the HTML attribute path — the
64
+ * canonical authoring shape for a2ui-generated markup. Default
65
+ * UIElement attribute reflection would assign the string verbatim
66
+ * and break the array shape; parse here.
67
+ */
68
+ attributeChangedCallback(name, oldVal, newVal) {
69
+ if (name === 'integrations') {
70
+ if (newVal == null) {
71
+ this.integrations = [];
72
+ } else {
73
+ try {
74
+ const parsed = JSON.parse(newVal);
75
+ this.integrations = Array.isArray(parsed) ? parsed : [];
76
+ } catch {
77
+ this.integrations = [];
78
+ }
79
+ }
80
+ return;
81
+ }
82
+ super.attributeChangedCallback(name, oldVal, newVal);
83
+ }
84
+
85
+ // ── lifecycle ───────────────────────────────────────────────────
86
+
87
+ connected() {
88
+ // Landmark + label so SR announces the region.
89
+ this.setAttribute('role', 'region');
90
+ this.setAttribute('aria-labelledby', this.#headerId);
91
+ if (!this.hasAttribute('aria-label')) {
92
+ this.setAttribute('aria-label', 'Integrations');
93
+ }
94
+
95
+ // Late-parse JSON attribute (UIElement assigns the raw String on
96
+ // first connect; we re-parse here).
97
+ if (this.hasAttribute('integrations') && !Array.isArray(this.integrations)) {
98
+ try { this.integrations = JSON.parse(this.getAttribute('integrations')); }
99
+ catch { this.integrations = []; }
100
+ }
101
+
102
+ this.addEventListener('input', this.#onSearchInput);
103
+ this.addEventListener('change', this.#onFilterChange);
104
+ }
105
+
106
+ disconnected() {
107
+ this.removeEventListener('input', this.#onSearchInput);
108
+ this.removeEventListener('change', this.#onFilterChange);
109
+ if (this.#searchTimer) {
110
+ clearTimeout(this.#searchTimer);
111
+ this.#searchTimer = null;
112
+ }
113
+ }
114
+
115
+ render() {
116
+ this.#syncEmptyAttrs();
117
+ // The composite re-stamps the <segmented-ui> + its <segment-ui> children on
118
+ // every render, but segmented-ui's own render() — which applies [selected]
119
+ // to the active segment — does NOT re-run after a parent re-stamp. Without
120
+ // this, the selected segment loses [selected] and its label renders in the
121
+ // dim unselected color on the light pill (bug-52). render() runs post-stamp
122
+ // (element.js: stamp() then render()), so the segmented-ui is freshly mounted.
123
+ this.querySelector('segmented-ui[data-part="filter"]')?.render?.();
124
+ }
125
+
126
+ // ── render ──────────────────────────────────────────────────────
127
+
128
+ _renderTemplate() {
129
+ const placeholder = this['search-placeholder'] || 'Search integrations';
130
+ const filterValue = this['status-filter'] || 'all';
131
+ const isLoading = !!this.loading;
132
+ const groups = this.#computeGroups();
133
+
134
+ return html`
135
+ <header data-part="header">
136
+ <h2 data-part="title" id=${this.#headerId}>Integrations</h2>
137
+ <slot name="header"></slot>
138
+ </header>
139
+
140
+ <div data-part="toolbar" role="search">
141
+ <search-ui
142
+ data-part="search"
143
+ placeholder=${placeholder}
144
+ value=${this.#query}
145
+ aria-label="Search integrations"
146
+ ></search-ui>
147
+
148
+ <segmented-ui
149
+ data-part="filter"
150
+ value=${filterValue}
151
+ aria-label="Filter integrations by status"
152
+ >
153
+ ${FILTER_OPTIONS.map((opt) => html`
154
+ <segment-ui
155
+ value=${opt.value}
156
+ text=${opt.label}
157
+ data-filter=${opt.value}
158
+ ></segment-ui>
159
+ `)}
160
+ </segmented-ui>
161
+ </div>
162
+
163
+ ${isLoading
164
+ ? this.#renderSkeleton()
165
+ : this.#renderGroupsOrEmpty(groups)}
166
+ `;
167
+ }
168
+
169
+ #renderSkeleton() {
170
+ const tiles = [];
171
+ for (let i = 0; i < SKELETON_COUNT; i++) {
172
+ tiles.push(html`
173
+ <div data-part="skeleton-tile" aria-hidden="true"></div>
174
+ `);
175
+ }
176
+ return html`
177
+ <div data-part="skeleton" aria-busy="true" aria-live="polite">
178
+ <grid-ui columns="1 2@sm 3@lg" gap="md">
179
+ ${tiles}
180
+ </grid-ui>
181
+ </div>
182
+ `;
183
+ }
184
+
185
+ #renderGroupsOrEmpty(groups) {
186
+ const totalVisible = groups.reduce((sum, g) => sum + g.items.length, 0);
187
+ const integrationsArr = Array.isArray(this.integrations) ? this.integrations : [];
188
+ if (totalVisible === 0) {
189
+ const hasData = integrationsArr.length > 0;
190
+ const reason = hasData ? 'empty-search' : 'empty-data';
191
+ return this.#renderEmpty(reason);
192
+ }
193
+ return html`
194
+ <div data-part="sections">
195
+ ${groups.map((g) => this.#renderSection(g))}
196
+ </div>
197
+ `;
198
+ }
199
+
200
+ #renderSection(group) {
201
+ const { label, items, key } = group;
202
+ // group-by="none" → single ungrouped grid, no heading
203
+ if (group.key === '__none__') {
204
+ return html`
205
+ <section data-part="section" data-group="__none__">
206
+ <grid-ui columns="1 2@sm 3@lg" gap="md">
207
+ ${items.map((item) => this.#renderCard(item))}
208
+ </grid-ui>
209
+ </section>
210
+ `;
211
+ }
212
+ return html`
213
+ <section data-part="section" data-group=${key} role="group" aria-label=${label}>
214
+ <text-ui data-part="section-heading" variant="kicker">${label}</text-ui>
215
+ <grid-ui columns="1 2@sm 3@lg" gap="md">
216
+ ${items.map((item) => this.#renderCard(item))}
217
+ </grid-ui>
218
+ </section>
219
+ `;
220
+ }
221
+
222
+ #renderCard(item) {
223
+ // The card primitive expects flat string attributes; map our descriptor
224
+ // shape onto its prop contract. We let card-ui handle status-driven
225
+ // button rendering, badge, and event emission.
226
+ const provider = String(item?.provider ?? '');
227
+ const name = String(item?.name ?? '');
228
+ const description = String(item?.description ?? '');
229
+ const logo = String(item?.logo ?? '');
230
+ const status = String(item?.status ?? 'available');
231
+ const errMsg = String(item?.['error-message'] ?? item?.errorMessage ?? '');
232
+ return html`
233
+ <integration-card-ui
234
+ data-provider=${provider}
235
+ provider=${provider}
236
+ name=${name}
237
+ description=${description}
238
+ logo=${logo}
239
+ status=${status}
240
+ error-message=${errMsg}
241
+ ></integration-card-ui>
242
+ `;
243
+ }
244
+
245
+ #renderEmpty(reason) {
246
+ const isSearch = reason === 'empty-search';
247
+ const heading = isSearch
248
+ ? `No integrations match "${this.#query}"`
249
+ : 'No integrations available yet';
250
+ const desc = isSearch
251
+ ? 'Try a different keyword or clear the search.'
252
+ : 'Integrations will appear here once they are available.';
253
+ return html`
254
+ <div data-part="empty" data-empty-reason=${reason} role="status">
255
+ <slot name="empty">
256
+ <text-ui data-part="empty-heading" variant="heading">${heading}</text-ui>
257
+ <text-ui data-part="empty-description" variant="body" color="subtle">${desc}</text-ui>
258
+ </slot>
259
+ </div>
260
+ `;
261
+ }
262
+
263
+ // ── data shaping ────────────────────────────────────────────────
264
+
265
+ /**
266
+ * Apply search + status-filter, then bucket by `group-by`. Returns
267
+ * an array of `{ key, label, items }`.
268
+ *
269
+ * `group-by="none"` returns a single synthetic group with key
270
+ * `__none__` so the renderer can skip the heading.
271
+ */
272
+ #computeGroups() {
273
+ const all = Array.isArray(this.integrations) ? this.integrations : [];
274
+ const q = (this.#query || '').trim().toLowerCase();
275
+ const statusFilter = this['status-filter'] || 'all';
276
+
277
+ const filtered = all.filter((item) => {
278
+ if (!item || typeof item !== 'object') return false;
279
+ // Status filter
280
+ if (statusFilter !== 'all') {
281
+ const itemStatus = String(item.status ?? 'available');
282
+ if (itemStatus !== statusFilter) return false;
283
+ }
284
+ // Search filter — matches name, description, provider, category
285
+ if (q) {
286
+ const hay = [
287
+ item.name,
288
+ item.description,
289
+ item.provider,
290
+ item.category,
291
+ ].map((s) => String(s ?? '').toLowerCase()).join(' ');
292
+ if (!hay.includes(q)) return false;
293
+ }
294
+ return true;
295
+ });
296
+
297
+ const groupBy = this['group-by'] || 'category';
298
+ if (groupBy === 'none') {
299
+ return [{ key: '__none__', label: '', items: filtered }];
300
+ }
301
+
302
+ // Group by the chosen key. Preserve first-seen order so the section
303
+ // order is stable across re-renders.
304
+ const order = [];
305
+ const map = new Map();
306
+ for (const item of filtered) {
307
+ const raw = groupBy === 'status'
308
+ ? String(item.status ?? 'available')
309
+ : String(item.category ?? 'Other');
310
+ const key = raw || 'Other';
311
+ if (!map.has(key)) {
312
+ map.set(key, []);
313
+ order.push(key);
314
+ }
315
+ map.get(key).push(item);
316
+ }
317
+ return order.map((key) => ({
318
+ key,
319
+ label: groupBy === 'status' ? this.#statusLabel(key) : key,
320
+ items: map.get(key),
321
+ }));
322
+ }
323
+
324
+ #statusLabel(status) {
325
+ switch (status) {
326
+ case 'available': return 'Available';
327
+ case 'connected': return 'Connected';
328
+ case 'error': return 'Issues';
329
+ case 'pending': return 'Connecting';
330
+ case 'coming-soon': return 'Coming soon';
331
+ default: return status || 'Other';
332
+ }
333
+ }
334
+
335
+ #syncEmptyAttrs() {
336
+ const all = Array.isArray(this.integrations) ? this.integrations : [];
337
+ const groups = this.#computeGroups();
338
+ const totalVisible = groups.reduce((sum, g) => sum + g.items.length, 0);
339
+ if (this.loading) {
340
+ this.removeAttribute('data-empty-search');
341
+ this.removeAttribute('data-empty-data');
342
+ return;
343
+ }
344
+ if (totalVisible === 0 && all.length > 0) {
345
+ this.setAttribute('data-empty-search', '');
346
+ this.removeAttribute('data-empty-data');
347
+ } else if (all.length === 0) {
348
+ this.setAttribute('data-empty-data', '');
349
+ this.removeAttribute('data-empty-search');
350
+ } else {
351
+ this.removeAttribute('data-empty-search');
352
+ this.removeAttribute('data-empty-data');
353
+ }
354
+ }
355
+
356
+ // ── event delegation ────────────────────────────────────────────
357
+
358
+ /**
359
+ * Listen for the inner <search-ui> emitting `input`. We track the
360
+ * query locally (drives the filter), debounce the upward `search`
361
+ * emit so external listeners aren't spammed.
362
+ */
363
+ #onSearchInput = (e) => {
364
+ const target = e.target;
365
+ if (!target || !target.closest) return;
366
+ const search = target.closest('search-ui[data-part="search"]');
367
+ if (!search || !this.contains(search)) return;
368
+
369
+ const value = String(e.detail?.value ?? search.value ?? '');
370
+ this.#query = value;
371
+
372
+ // Trigger a re-render to apply the filter. UIElement reactivity is
373
+ // property-based; we toggle a private signal indirectly by setting
374
+ // a dummy reflection here is overkill — just request a render via
375
+ // a no-op property assignment. Simpler: mutate the array reference.
376
+ this.integrations = Array.isArray(this.integrations) ? [...this.integrations] : [];
377
+
378
+ // Debounce upward emit so consumers don't get spammed.
379
+ if (this.#searchTimer) clearTimeout(this.#searchTimer);
380
+ this.#searchTimer = setTimeout(() => {
381
+ this.dispatchEvent(new CustomEvent('search', {
382
+ bubbles: true,
383
+ detail: { query: value.toLowerCase().trim() },
384
+ }));
385
+ }, 250);
386
+ };
387
+
388
+ /**
389
+ * Listen for the inner <segmented-ui> emitting `change` with a new
390
+ * value. Update [status-filter] + re-render + emit `filter-change`.
391
+ */
392
+ #onFilterChange = (e) => {
393
+ const target = e.target;
394
+ if (!target || !target.closest) return;
395
+ const segmented = target.closest('segmented-ui[data-part="filter"]');
396
+ if (!segmented || !this.contains(segmented)) return;
397
+
398
+ const value = String(e.detail?.value ?? segmented.value ?? 'all');
399
+ if (this['status-filter'] === value) return;
400
+ this['status-filter'] = value;
401
+
402
+ this.dispatchEvent(new CustomEvent('filter-change', {
403
+ bubbles: true,
404
+ detail: { status: value },
405
+ }));
406
+ };
407
+ }
408
+
409
+ if (!customElements.get('integrations-page-ui')) {
410
+ customElements.define('integrations-page-ui', UIIntegrationsPage);
411
+ }
412
+
413
+ export { UIIntegrationsPage };