@adia-ai/web-modules 0.8.35 → 0.8.37
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/CHANGELOG.md +22 -0
- package/README.md +3 -3
- package/agent-admin/admin-roster/admin-roster.js +1 -1
- package/agent-admin/admin-settings/admin-settings.js +1 -1
- package/agent-admin/agent-admin/agent-admin.js +1 -1
- package/billing/billing-overview/billing-overview.a2ui.json +281 -0
- package/billing/billing-overview/billing-overview.class.js +838 -0
- package/billing/billing-overview/billing-overview.css +223 -0
- package/billing/billing-overview/billing-overview.js +16 -0
- package/billing/billing-overview/billing-overview.yaml +441 -0
- package/billing/index.js +21 -0
- package/billing/invoice-detail/invoice-detail.a2ui.json +276 -0
- package/billing/invoice-detail/invoice-detail.class.js +711 -0
- package/billing/invoice-detail/invoice-detail.css +305 -0
- package/billing/invoice-detail/invoice-detail.js +16 -0
- package/billing/invoice-detail/invoice-detail.yaml +411 -0
- package/billing/invoice-history/invoice-history.a2ui.json +315 -0
- package/billing/invoice-history/invoice-history.class.js +672 -0
- package/billing/invoice-history/invoice-history.css +130 -0
- package/billing/invoice-history/invoice-history.js +16 -0
- package/billing/invoice-history/invoice-history.yaml +398 -0
- package/billing/payment-method-form/payment-method-form.a2ui.json +310 -0
- package/billing/payment-method-form/payment-method-form.class.js +886 -0
- package/billing/payment-method-form/payment-method-form.css +149 -0
- package/billing/payment-method-form/payment-method-form.js +16 -0
- package/billing/payment-method-form/payment-method-form.yaml +430 -0
- package/billing/payment-method-list/payment-method-list.a2ui.json +283 -0
- package/billing/payment-method-list/payment-method-list.class.js +705 -0
- package/billing/payment-method-list/payment-method-list.css +128 -0
- package/billing/payment-method-list/payment-method-list.js +16 -0
- package/billing/payment-method-list/payment-method-list.yaml +355 -0
- package/billing/plan-picker/plan-picker.a2ui.json +349 -0
- package/billing/plan-picker/plan-picker.class.js +635 -0
- package/billing/plan-picker/plan-picker.css +227 -0
- package/billing/plan-picker/plan-picker.js +16 -0
- package/billing/plan-picker/plan-picker.yaml +453 -0
- package/chat/README.md +1 -1
- package/chat/chat-composer/chat-composer.js +2 -2
- package/chat/chat-empty/chat-empty.js +2 -2
- package/chat/chat-shell/chat-shell.js +4 -4
- package/chat/chat-sidebar/chat-sidebar.js +2 -2
- package/chat/chat-surfaces/chat-surfaces.js +2 -2
- package/chat/chat-thread/chat-thread.js +2 -2
- package/dashboard/dashboard-layout/dashboard-layout.a2ui.json +254 -0
- package/dashboard/dashboard-layout/dashboard-layout.css +190 -0
- package/dashboard/dashboard-layout/dashboard-layout.js +289 -0
- package/dashboard/dashboard-layout/dashboard-layout.yaml +374 -0
- package/dashboard/date-range-selector/date-range-selector.a2ui.json +236 -0
- package/dashboard/date-range-selector/date-range-selector.css +84 -0
- package/dashboard/date-range-selector/date-range-selector.js +686 -0
- package/dashboard/date-range-selector/date-range-selector.yaml +358 -0
- package/dashboard/index.js +14 -0
- package/dist/everything.min.js +1 -1
- package/editor/editor-canvas/editor-canvas.js +2 -2
- package/editor/editor-shell/editor-shell.js +2 -2
- package/editor/editor-sidebar/editor-sidebar.js +2 -2
- package/editor/editor-toolbar/editor-toolbar.js +2 -2
- package/feedback/confirm-dialog/confirm-dialog.a2ui.json +206 -0
- package/feedback/confirm-dialog/confirm-dialog.css +157 -0
- package/feedback/confirm-dialog/confirm-dialog.js +383 -0
- package/feedback/confirm-dialog/confirm-dialog.yaml +297 -0
- package/feedback/index.js +10 -0
- package/form/form-popover/form-popover.js +1 -1
- package/generative/index.d.ts +1 -1
- package/generative/index.js +2 -2
- package/onboarding/index.js +1 -0
- package/onboarding/onboarding-checklist/onboarding-checklist.a2ui.json +263 -0
- package/onboarding/onboarding-checklist/onboarding-checklist.css +157 -0
- package/onboarding/onboarding-checklist/onboarding-checklist.js +428 -0
- package/onboarding/onboarding-checklist/onboarding-checklist.yaml +236 -0
- package/package.json +99 -6
- package/runtime/a2ui-root/a2ui-root.js +3 -3
- package/runtime/gen-root/gen-root.js +5 -5
- package/settings/index.js +12 -0
- package/settings/integrations-page/integrations-page.a2ui.json +274 -0
- package/settings/integrations-page/integrations-page.css +177 -0
- package/settings/integrations-page/integrations-page.d.ts +77 -0
- package/settings/integrations-page/integrations-page.js +413 -0
- package/settings/integrations-page/integrations-page.yaml +317 -0
- package/settings/notification-preferences/notification-preferences.a2ui.json +245 -0
- package/settings/notification-preferences/notification-preferences.css +204 -0
- package/settings/notification-preferences/notification-preferences.js +622 -0
- package/settings/notification-preferences/notification-preferences.yaml +358 -0
- package/shell/admin-command/admin-command.js +2 -2
- package/shell/admin-shell/admin-shell.js +2 -2
- package/shell/admin-sidebar/admin-sidebar.js +2 -2
- package/shell/embed-shell/embed-shell.js +3 -3
- package/simple/simple-shell/simple-shell.js +1 -1
- 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 };
|