@adia-ai/web-components 0.6.32 → 0.6.34
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 +44 -0
- package/components/accordion/accordion.css +2 -2
- package/components/action-list/action-list.css +2 -2
- package/components/agent-artifact/agent-artifact.css +31 -31
- package/components/agent-feedback-bar/agent-feedback-bar.css +10 -10
- package/components/agent-questions/agent-questions.css +57 -57
- package/components/agent-reasoning/agent-reasoning.css +62 -62
- package/components/agent-suggestions/agent-suggestions.css +4 -4
- package/components/agent-trace/agent-trace.css +53 -53
- package/components/alert/alert.css +41 -41
- package/components/avatar/avatar.css +27 -27
- package/components/badge/badge.css +27 -27
- package/components/block/block.css +16 -16
- package/components/breadcrumb/breadcrumb.css +23 -23
- package/components/button/button.css +101 -91
- package/components/calendar-grid/calendar-grid.a2ui.json +136 -0
- package/components/calendar-grid/calendar-grid.css +226 -0
- package/components/calendar-grid/calendar-grid.d.ts +37 -0
- package/components/calendar-grid/calendar-grid.js +17 -0
- package/components/calendar-grid/calendar-grid.yaml +116 -0
- package/components/calendar-grid/class.js +300 -0
- package/components/calendar-picker/calendar-picker.css +139 -139
- package/components/canvas/canvas.css +12 -12
- package/components/card/card.css +83 -83
- package/components/chart/chart.css +224 -224
- package/components/chart-legend/chart-legend.css +26 -26
- package/components/check/check.css +40 -40
- package/components/code/code.css +125 -125
- package/components/col/col.css +15 -15
- package/components/color-picker/color-picker.css +55 -55
- package/components/combobox/class.js +861 -0
- package/components/combobox/combobox.a2ui.json +363 -0
- package/components/combobox/combobox.css +244 -0
- package/components/combobox/combobox.d.ts +113 -0
- package/components/combobox/combobox.examples.md +59 -0
- package/components/combobox/combobox.js +17 -0
- package/components/combobox/combobox.test.js +181 -0
- package/components/combobox/combobox.yaml +369 -0
- package/components/command/command.css +90 -90
- package/components/date-range-picker/class.js +775 -0
- package/components/date-range-picker/date-range-picker.a2ui.json +300 -0
- package/components/date-range-picker/date-range-picker.css +178 -0
- package/components/date-range-picker/date-range-picker.d.ts +82 -0
- package/components/date-range-picker/date-range-picker.examples.md +37 -0
- package/components/date-range-picker/date-range-picker.js +17 -0
- package/components/date-range-picker/date-range-picker.test.js +387 -0
- package/components/date-range-picker/date-range-picker.yaml +285 -0
- package/components/datetime-picker/class.js +706 -0
- package/components/datetime-picker/datetime-picker.a2ui.json +334 -0
- package/components/datetime-picker/datetime-picker.css +150 -0
- package/components/datetime-picker/datetime-picker.d.ts +86 -0
- package/components/datetime-picker/datetime-picker.examples.md +46 -0
- package/components/datetime-picker/datetime-picker.js +17 -0
- package/components/datetime-picker/datetime-picker.test.js +454 -0
- package/components/datetime-picker/datetime-picker.yaml +332 -0
- package/components/demo-toggle/demo-toggle.css +27 -27
- package/components/description-list/description-list.css +18 -18
- package/components/divider/divider.css +24 -24
- package/components/embed/embed.css +6 -6
- package/components/empty-state/empty-state.css +27 -27
- package/components/feed/feed.css +12 -12
- package/components/field/field.css +37 -28
- package/components/field/field.test.js +32 -0
- package/components/fields/fields.css +5 -5
- package/components/grid/grid.css +5 -5
- package/components/heatmap/heatmap.css +63 -63
- package/components/icon/icon.css +12 -12
- package/components/image/image.css +14 -14
- package/components/index.js +8 -0
- package/components/input/input.css +66 -66
- package/components/inspector/inspector.css +6 -6
- package/components/integration-card/class.js +410 -0
- package/components/integration-card/integration-card.a2ui.json +268 -0
- package/components/integration-card/integration-card.css +169 -0
- package/components/integration-card/integration-card.d.ts +63 -0
- package/components/integration-card/integration-card.examples.md +41 -0
- package/components/integration-card/integration-card.js +17 -0
- package/components/integration-card/integration-card.test.js +306 -0
- package/components/integration-card/integration-card.yaml +280 -0
- package/components/kbd/kbd.css +32 -32
- package/components/link/link.css +12 -12
- package/components/list/list.css +8 -8
- package/components/list-window/class.js +688 -0
- package/components/list-window/list-window.a2ui.json +277 -0
- package/components/list-window/list-window.css +124 -0
- package/components/list-window/list-window.d.ts +84 -0
- package/components/list-window/list-window.examples.md +73 -0
- package/components/list-window/list-window.js +17 -0
- package/components/list-window/list-window.test.js +303 -0
- package/components/list-window/list-window.yaml +270 -0
- package/components/menu/menu.css +8 -8
- package/components/modal/modal.css +43 -43
- package/components/nav/nav.css +40 -40
- package/components/nav-group/nav-group.css +52 -52
- package/components/nav-item/nav-item.css +44 -44
- package/components/noodles/noodles.css +31 -31
- package/components/option-card/option-card.css +69 -69
- package/components/otp-input/otp-input.css +30 -30
- package/components/page/page.css +18 -18
- package/components/pagination/pagination.css +61 -61
- package/components/pane/pane.css +57 -57
- package/components/pipeline-status/pipeline-status.css +65 -65
- package/components/popover/popover.css +17 -17
- package/components/progress/progress.css +23 -23
- package/components/progress-row/progress-row.css +17 -17
- package/components/radio/radio.css +39 -39
- package/components/range/range.css +55 -55
- package/components/rating/rating.css +28 -28
- package/components/richtext/richtext.css +133 -133
- package/components/row/row.css +19 -19
- package/components/search/search.css +5 -5
- package/components/segment/segment.css +24 -24
- package/components/segmented/segmented.css +25 -25
- package/components/select/select.css +84 -84
- package/components/skeleton/skeleton.css +14 -14
- package/components/slider/slider.css +46 -46
- package/components/spinner/class.js +69 -0
- package/components/spinner/spinner.a2ui.json +197 -0
- package/components/spinner/spinner.css +165 -0
- package/components/spinner/spinner.d.ts +26 -0
- package/components/spinner/spinner.examples.md +26 -0
- package/components/spinner/spinner.js +17 -0
- package/components/spinner/spinner.test.js +234 -0
- package/components/spinner/spinner.yaml +230 -0
- package/components/stack/stack.css +11 -11
- package/components/stat/stat.css +25 -25
- package/components/step-progress/step-progress.css +20 -20
- package/components/stepper/stepper.css +29 -29
- package/components/stream/stream.css +12 -12
- package/components/swatch/swatch.css +68 -68
- package/components/swiper/swiper.css +57 -57
- package/components/switch/switch.css +52 -52
- package/components/table/class.js +9 -0
- package/components/table/table.a2ui.json +1 -1
- package/components/table/table.css +162 -162
- package/components/table/table.d.ts +1 -1
- package/components/table/table.test.js +53 -0
- package/components/table/table.yaml +13 -1
- package/components/table-toolbar/table-toolbar.css +32 -32
- package/components/tabs/tabs.css +51 -51
- package/components/tag/tag.css +48 -48
- package/components/text/text.css +44 -44
- package/components/textarea/textarea.css +46 -46
- package/components/time-picker/class.js +693 -0
- package/components/time-picker/time-picker.a2ui.json +267 -0
- package/components/time-picker/time-picker.css +122 -0
- package/components/time-picker/time-picker.d.ts +75 -0
- package/components/time-picker/time-picker.examples.md +35 -0
- package/components/time-picker/time-picker.js +17 -0
- package/components/time-picker/time-picker.test.js +287 -0
- package/components/time-picker/time-picker.yaml +256 -0
- package/components/timeline/timeline.css +50 -50
- package/components/toast/toast.css +58 -58
- package/components/toggle-group/toggle-group.css +6 -6
- package/components/toggle-scheme/toggle-scheme.css +2 -2
- package/components/toolbar/toolbar.css +17 -17
- package/components/tooltip/tooltip.css +2 -2
- package/components/tree/tree.css +37 -37
- package/components/upload/upload.css +49 -49
- package/dist/icons-manifest.js +3 -3
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +121 -83
- package/package.json +1 -1
- package/styles/components.css +8 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-side-effect class export for `<integration-card-ui>`.
|
|
3
|
+
*
|
|
4
|
+
* Importing this file gives you the class(es) without auto-registering the tag.
|
|
5
|
+
* Useful for test isolation, subclassing with tag-name override, or selective
|
|
6
|
+
* composition.
|
|
7
|
+
*
|
|
8
|
+
* The auto-register path stays at `@adia-ai/web-components/components/integration-card`
|
|
9
|
+
* (which imports this file + calls `defineIfFree()`).
|
|
10
|
+
*
|
|
11
|
+
* @see ../../USAGE.md#registration--auto-vs-explicit
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* <integration-card-ui provider="slack" name="Slack"
|
|
16
|
+
* logo="/integrations/slack.svg"
|
|
17
|
+
* description="Send AI replies and notifications to channels."
|
|
18
|
+
* status="available">
|
|
19
|
+
* </integration-card-ui>
|
|
20
|
+
*
|
|
21
|
+
* Single tile representing one third-party integration. Status drives the
|
|
22
|
+
* button label + variant; tile chrome borrows from <card-ui> but the
|
|
23
|
+
* primitive is its own surface (one tile = one provider, never compose
|
|
24
|
+
* <card-ui> around it).
|
|
25
|
+
*
|
|
26
|
+
* Events:
|
|
27
|
+
* connect — { detail: { provider } } fired when `status="available"`
|
|
28
|
+
* configure — { detail: { provider } } fired when `status="connected"`
|
|
29
|
+
* retry — { detail: { provider } } fired when `status="error"`
|
|
30
|
+
* disconnect — bubbled from consumer-provided overflow menu items
|
|
31
|
+
* with `data-action="disconnect"` in the `actions` slot
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { UIElement } from '../../core/element.js';
|
|
35
|
+
|
|
36
|
+
const STATUS = Object.freeze({
|
|
37
|
+
AVAILABLE: 'available',
|
|
38
|
+
CONNECTED: 'connected',
|
|
39
|
+
ERROR: 'error',
|
|
40
|
+
PENDING: 'pending',
|
|
41
|
+
COMING_SOON: 'coming-soon',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const ACTION_FOR_STATUS = Object.freeze({
|
|
45
|
+
available: { event: 'press', emits: 'connect', label: 'Connect', variant: 'primary', icon: '' },
|
|
46
|
+
connected: { event: 'press', emits: 'configure', label: 'Configure', variant: 'outline', icon: '' },
|
|
47
|
+
error: { event: 'press', emits: 'retry', label: 'Retry', variant: 'outline', icon: 'arrow-clockwise' },
|
|
48
|
+
pending: { event: 'press', emits: '', label: 'Connecting…', variant: 'outline', icon: '' },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const BADGE_FOR_STATUS = Object.freeze({
|
|
52
|
+
connected: { variant: 'success', text: 'Connected', icon: 'check' },
|
|
53
|
+
error: { variant: 'danger', text: 'Error', icon: 'warning-circle' },
|
|
54
|
+
pending: { variant: 'info', text: 'Connecting…', icon: '' },
|
|
55
|
+
'coming-soon':{ variant: 'muted', text: 'Coming soon', icon: '' },
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export class UIIntegrationCard extends UIElement {
|
|
59
|
+
// Phosphor icons this primitive auto-stamps (without consumer markup).
|
|
60
|
+
// Aggregated by installIconLoadersForRegistered() across all defined
|
|
61
|
+
// elements. Audited by check-required-icons.mjs (slot 11).
|
|
62
|
+
static requiredIcons = ['check', 'warning-circle', 'arrow-clockwise'];
|
|
63
|
+
|
|
64
|
+
static properties = {
|
|
65
|
+
provider: { type: String, default: '', reflect: true },
|
|
66
|
+
name: { type: String, default: '', reflect: false },
|
|
67
|
+
logo: { type: String, default: '', reflect: false },
|
|
68
|
+
description: { type: String, default: '', reflect: false },
|
|
69
|
+
status: { type: String, default: 'available', reflect: true },
|
|
70
|
+
'error-message': { type: String, default: '', attribute: 'error-message' },
|
|
71
|
+
disabled: { type: Boolean, default: false, reflect: true },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
static parts = {
|
|
75
|
+
body: '<div slot="body" data-integration-card-body></div>',
|
|
76
|
+
logo: '<div slot="logo" data-integration-card-logo></div>',
|
|
77
|
+
heading: '<div slot="heading" data-integration-card-heading></div>',
|
|
78
|
+
desc: '<p slot="description" data-integration-card-description></p>',
|
|
79
|
+
error: '<p slot="error" data-integration-card-error></p>',
|
|
80
|
+
status: '<badge-ui slot="status" data-integration-card-status></badge-ui>',
|
|
81
|
+
footer: '<footer slot="footer" data-integration-card-footer></footer>',
|
|
82
|
+
button: '<button-ui slot="action" data-integration-card-action></button-ui>',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
static template = () => null;
|
|
86
|
+
|
|
87
|
+
// Stable ID for aria-labelledby pointing at the provider name.
|
|
88
|
+
#headingId = '';
|
|
89
|
+
|
|
90
|
+
// Cached element refs (lazily filled in `connected()`).
|
|
91
|
+
#bodyEl = null;
|
|
92
|
+
#logoEl = null;
|
|
93
|
+
#headingEl = null;
|
|
94
|
+
#descEl = null;
|
|
95
|
+
#errorEl = null;
|
|
96
|
+
#statusEl = null;
|
|
97
|
+
#footerEl = null;
|
|
98
|
+
#buttonEl = null;
|
|
99
|
+
|
|
100
|
+
connected() {
|
|
101
|
+
// Group semantic — labeled region announced by SR.
|
|
102
|
+
this.setAttribute('role', 'group');
|
|
103
|
+
|
|
104
|
+
// Build the static frame once. ensure() is idempotent.
|
|
105
|
+
this.#bodyEl = this.ensure('body');
|
|
106
|
+
this.#logoEl = this.ensure('logo');
|
|
107
|
+
this.#headingEl = this.ensure('heading');
|
|
108
|
+
this.#descEl = this.ensure('desc');
|
|
109
|
+
this.#statusEl = this.ensure('status');
|
|
110
|
+
this.#footerEl = this.ensure('footer');
|
|
111
|
+
|
|
112
|
+
// Slot/structural arrangement: logo + heading + status sit inside body row;
|
|
113
|
+
// description sits below; error sits below description; button sits in footer.
|
|
114
|
+
// We arrange via DOM order so CSS direct-child selectors apply consistently.
|
|
115
|
+
if (this.#logoEl.parentElement !== this.#bodyEl) this.#bodyEl.appendChild(this.#logoEl);
|
|
116
|
+
if (this.#headingEl.parentElement !== this.#bodyEl) this.#bodyEl.appendChild(this.#headingEl);
|
|
117
|
+
if (this.#statusEl.parentElement !== this.#bodyEl) this.#bodyEl.appendChild(this.#statusEl);
|
|
118
|
+
|
|
119
|
+
// Assign a stable id for aria-labelledby (don't clobber author-set id).
|
|
120
|
+
if (!this.#headingEl.id) {
|
|
121
|
+
this.#headingId = `integration-card-heading-${Math.random().toString(36).slice(2, 9)}`;
|
|
122
|
+
this.#headingEl.id = this.#headingId;
|
|
123
|
+
} else {
|
|
124
|
+
this.#headingId = this.#headingEl.id;
|
|
125
|
+
}
|
|
126
|
+
this.setAttribute('aria-labelledby', this.#headingId);
|
|
127
|
+
|
|
128
|
+
// Wire action button activation. The button-ui dispatches `press`; we
|
|
129
|
+
// re-dispatch a typed event (connect / configure / retry) based on status.
|
|
130
|
+
this.addEventListener('press', this.#onPress);
|
|
131
|
+
|
|
132
|
+
// Catch consumer-wired overflow actions in the `actions` slot. The
|
|
133
|
+
// menu-ui dispatches `action` with the action name in detail — when
|
|
134
|
+
// it's `disconnect`, we re-dispatch as a typed event with the
|
|
135
|
+
// provider key. Same recipe for `reauth` / `re-authenticate`.
|
|
136
|
+
this.addEventListener('action', this.#onMenuAction);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
disconnected() {
|
|
140
|
+
this.removeEventListener('press', this.#onPress);
|
|
141
|
+
this.removeEventListener('action', this.#onMenuAction);
|
|
142
|
+
this.#bodyEl = null;
|
|
143
|
+
this.#logoEl = null;
|
|
144
|
+
this.#headingEl = null;
|
|
145
|
+
this.#descEl = null;
|
|
146
|
+
this.#errorEl = null;
|
|
147
|
+
this.#statusEl = null;
|
|
148
|
+
this.#footerEl = null;
|
|
149
|
+
this.#buttonEl = null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
render() {
|
|
153
|
+
if (!this.#headingEl) return;
|
|
154
|
+
|
|
155
|
+
const status = this.#normalizedStatus();
|
|
156
|
+
|
|
157
|
+
// ── Heading ──────────────────────────────────────────────────────
|
|
158
|
+
if (this.name && this.#headingEl.textContent !== this.name) {
|
|
159
|
+
this.#headingEl.textContent = this.name;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Description ─────────────────────────────────────────────────
|
|
163
|
+
// The default slot supersedes the description prop when non-empty.
|
|
164
|
+
// We detect "consumer-authored description" via any direct child that
|
|
165
|
+
// isn't one of our stamped data-integration-card-* elements and isn't
|
|
166
|
+
// a slotted action / menu.
|
|
167
|
+
const hasCustomDesc = this.#hasConsumerDescription();
|
|
168
|
+
if (hasCustomDesc) {
|
|
169
|
+
if (this.#descEl.parentElement === this) {
|
|
170
|
+
this.#descEl.remove();
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
if (!this.#descEl.parentElement) this.appendChild(this.#descEl);
|
|
174
|
+
if (this.description) {
|
|
175
|
+
if (this.#descEl.textContent !== this.description) {
|
|
176
|
+
this.#descEl.textContent = this.description;
|
|
177
|
+
}
|
|
178
|
+
this.#descEl.hidden = false;
|
|
179
|
+
} else {
|
|
180
|
+
this.#descEl.textContent = '';
|
|
181
|
+
this.#descEl.hidden = true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Logo ────────────────────────────────────────────────────────
|
|
186
|
+
this.#renderLogo();
|
|
187
|
+
|
|
188
|
+
// ── Status badge ────────────────────────────────────────────────
|
|
189
|
+
this.#renderStatusBadge(status);
|
|
190
|
+
|
|
191
|
+
// ── Error message (status="error" only) ─────────────────────────
|
|
192
|
+
this.#renderError(status);
|
|
193
|
+
|
|
194
|
+
// ── Action button (label + variant + visibility) ────────────────
|
|
195
|
+
this.#renderActionButton(status);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Private rendering helpers ─────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
#normalizedStatus() {
|
|
201
|
+
const s = (this.status || '').trim() || STATUS.AVAILABLE;
|
|
202
|
+
// Validate against the enum; fall back to `available` on bad input.
|
|
203
|
+
if (Object.values(STATUS).includes(s)) return s;
|
|
204
|
+
return STATUS.AVAILABLE;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#hasConsumerDescription() {
|
|
208
|
+
// Author description = direct child element that is neither one of our
|
|
209
|
+
// stamped data- markers nor an [slot="actions"] / [slot="action"] node.
|
|
210
|
+
for (const child of this.children) {
|
|
211
|
+
if (child === this.#bodyEl) continue;
|
|
212
|
+
if (child === this.#descEl) continue;
|
|
213
|
+
if (child === this.#errorEl) continue;
|
|
214
|
+
if (child === this.#footerEl) continue;
|
|
215
|
+
if (child.getAttribute && child.getAttribute('slot') === 'actions') continue;
|
|
216
|
+
if (child.dataset && (
|
|
217
|
+
child.dataset.integrationCardBody !== undefined ||
|
|
218
|
+
child.dataset.integrationCardDescription !== undefined ||
|
|
219
|
+
child.dataset.integrationCardError !== undefined ||
|
|
220
|
+
child.dataset.integrationCardFooter !== undefined
|
|
221
|
+
)) continue;
|
|
222
|
+
// Any other direct child counts as consumer-authored description content.
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#renderLogo() {
|
|
229
|
+
if (!this.#logoEl) return;
|
|
230
|
+
const logo = (this.logo || '').trim();
|
|
231
|
+
|
|
232
|
+
// No logo → strip any prior content and hide.
|
|
233
|
+
if (!logo) {
|
|
234
|
+
this.#logoEl.replaceChildren();
|
|
235
|
+
this.#logoEl.hidden = true;
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.#logoEl.hidden = false;
|
|
239
|
+
|
|
240
|
+
// URL vs icon-name sniff: presence of '/' → URL.
|
|
241
|
+
const isUrl = logo.includes('/');
|
|
242
|
+
|
|
243
|
+
if (isUrl) {
|
|
244
|
+
// Reuse existing <img> if same src; otherwise re-stamp.
|
|
245
|
+
let img = this.#logoEl.querySelector(':scope > img');
|
|
246
|
+
if (img && img.getAttribute('src') === logo) {
|
|
247
|
+
img.setAttribute('alt', `${this.name || this.provider || 'Integration'} logo`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
this.#logoEl.replaceChildren();
|
|
251
|
+
img = document.createElement('img');
|
|
252
|
+
img.setAttribute('src', logo);
|
|
253
|
+
img.setAttribute('alt', `${this.name || this.provider || 'Integration'} logo`);
|
|
254
|
+
img.setAttribute('data-integration-logo', '');
|
|
255
|
+
img.setAttribute('loading', 'lazy');
|
|
256
|
+
img.setAttribute('decoding', 'async');
|
|
257
|
+
this.#logoEl.appendChild(img);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Icon name → <icon-ui>.
|
|
262
|
+
let icon = this.#logoEl.querySelector(':scope > icon-ui');
|
|
263
|
+
if (icon && icon.getAttribute('name') === logo) return;
|
|
264
|
+
this.#logoEl.replaceChildren();
|
|
265
|
+
icon = document.createElement('icon-ui');
|
|
266
|
+
icon.setAttribute('name', logo);
|
|
267
|
+
icon.setAttribute('aria-hidden', 'true');
|
|
268
|
+
this.#logoEl.appendChild(icon);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#renderStatusBadge(status) {
|
|
272
|
+
if (!this.#statusEl) return;
|
|
273
|
+
const cfg = BADGE_FOR_STATUS[status];
|
|
274
|
+
if (!cfg) {
|
|
275
|
+
// No badge for `available` — keep DOM but hide.
|
|
276
|
+
this.#statusEl.hidden = true;
|
|
277
|
+
this.#statusEl.removeAttribute('aria-label');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
this.#statusEl.hidden = false;
|
|
281
|
+
this.#statusEl.setAttribute('variant', cfg.variant);
|
|
282
|
+
this.#statusEl.setAttribute('text', cfg.text);
|
|
283
|
+
this.#statusEl.setAttribute('size', 'sm');
|
|
284
|
+
if (cfg.icon) this.#statusEl.setAttribute('icon', cfg.icon);
|
|
285
|
+
else this.#statusEl.removeAttribute('icon');
|
|
286
|
+
// Explicit aria-label so SR announces "Connected" even when text rendering
|
|
287
|
+
// is delayed by icon-registry boot.
|
|
288
|
+
this.#statusEl.setAttribute('aria-label', cfg.text);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
#renderError(status) {
|
|
292
|
+
const msg = (this['error-message'] || this.getAttribute('error-message') || '').trim();
|
|
293
|
+
if (status === STATUS.ERROR && msg) {
|
|
294
|
+
if (!this.#errorEl) this.#errorEl = this.ensure('error');
|
|
295
|
+
if (!this.#errorEl.parentElement) {
|
|
296
|
+
// Insert after description, before footer if possible.
|
|
297
|
+
if (this.#descEl?.parentElement === this) {
|
|
298
|
+
this.#descEl.after(this.#errorEl);
|
|
299
|
+
} else if (this.#footerEl?.parentElement === this) {
|
|
300
|
+
this.insertBefore(this.#errorEl, this.#footerEl);
|
|
301
|
+
} else {
|
|
302
|
+
this.appendChild(this.#errorEl);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (this.#errorEl.textContent !== msg) this.#errorEl.textContent = msg;
|
|
306
|
+
this.#errorEl.hidden = false;
|
|
307
|
+
this.#errorEl.setAttribute('role', 'status');
|
|
308
|
+
} else if (this.#errorEl) {
|
|
309
|
+
this.#errorEl.hidden = true;
|
|
310
|
+
this.#errorEl.textContent = '';
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
#renderActionButton(status) {
|
|
315
|
+
// coming-soon → hide the button entirely; the badge carries the label.
|
|
316
|
+
if (status === STATUS.COMING_SOON) {
|
|
317
|
+
this.drop('button');
|
|
318
|
+
this.#buttonEl = null;
|
|
319
|
+
// Footer with no button can still hold the [slot="actions"] menu,
|
|
320
|
+
// so we keep the footer node attached.
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const cfg = ACTION_FOR_STATUS[status];
|
|
325
|
+
if (!cfg) {
|
|
326
|
+
this.drop('button');
|
|
327
|
+
this.#buttonEl = null;
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Ensure the button exists and sits inside the footer (light DOM
|
|
332
|
+
// positioning: button is a direct child of footer, footer is a direct
|
|
333
|
+
// child of host).
|
|
334
|
+
if (!this.#buttonEl) this.#buttonEl = this.ensure('button');
|
|
335
|
+
if (this.#buttonEl.parentElement !== this.#footerEl) {
|
|
336
|
+
this.#footerEl.insertBefore(this.#buttonEl, this.#footerEl.firstChild);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
this.#buttonEl.setAttribute('text', cfg.label);
|
|
340
|
+
this.#buttonEl.setAttribute('variant', cfg.variant);
|
|
341
|
+
this.#buttonEl.setAttribute('size', 'sm');
|
|
342
|
+
if (cfg.icon) this.#buttonEl.setAttribute('icon', cfg.icon);
|
|
343
|
+
else this.#buttonEl.removeAttribute('icon');
|
|
344
|
+
|
|
345
|
+
// Toggle semantics — aria-pressed reflects "is the integration on?"
|
|
346
|
+
// Provides screen-reader feedback on the action's effect.
|
|
347
|
+
if (status === STATUS.CONNECTED) {
|
|
348
|
+
this.#buttonEl.setAttribute('aria-pressed', 'true');
|
|
349
|
+
} else if (status === STATUS.AVAILABLE) {
|
|
350
|
+
this.#buttonEl.setAttribute('aria-pressed', 'false');
|
|
351
|
+
} else {
|
|
352
|
+
this.#buttonEl.removeAttribute('aria-pressed');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Explicit aria-label so SR announces "Connect to Slack" outside group ctx.
|
|
356
|
+
const verbForStatus = {
|
|
357
|
+
available: 'Connect to',
|
|
358
|
+
connected: 'Configure',
|
|
359
|
+
error: 'Retry connecting',
|
|
360
|
+
pending: 'Connecting to',
|
|
361
|
+
}[status] || cfg.label;
|
|
362
|
+
const labelTarget = this.name || this.provider || 'integration';
|
|
363
|
+
this.#buttonEl.setAttribute('aria-label', `${verbForStatus} ${labelTarget}`);
|
|
364
|
+
|
|
365
|
+
// pending → non-interactive; disabled prop → non-interactive too.
|
|
366
|
+
if (status === STATUS.PENDING || this.disabled) {
|
|
367
|
+
this.#buttonEl.setAttribute('disabled', '');
|
|
368
|
+
} else {
|
|
369
|
+
this.#buttonEl.removeAttribute('disabled');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ── Event handlers ────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
#onPress = (e) => {
|
|
376
|
+
// Only react to presses on OUR action button — overflow menu items
|
|
377
|
+
// dispatch `action` instead.
|
|
378
|
+
if (!this.#buttonEl) return;
|
|
379
|
+
if (e.target !== this.#buttonEl && !e.composedPath?.().includes(this.#buttonEl)) return;
|
|
380
|
+
if (this.disabled) { e.stopPropagation(); return; }
|
|
381
|
+
|
|
382
|
+
const status = this.#normalizedStatus();
|
|
383
|
+
if (status === STATUS.PENDING || status === STATUS.COMING_SOON) {
|
|
384
|
+
e.stopPropagation();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const cfg = ACTION_FOR_STATUS[status];
|
|
389
|
+
if (!cfg || !cfg.emits) return;
|
|
390
|
+
|
|
391
|
+
this.dispatchEvent(new CustomEvent(cfg.emits, {
|
|
392
|
+
bubbles: true,
|
|
393
|
+
detail: { provider: this.provider },
|
|
394
|
+
}));
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
#onMenuAction = (e) => {
|
|
398
|
+
// Bubbled `action` events from the consumer-provided overflow menu
|
|
399
|
+
// in [slot="actions"]. Detail shape from menu-item-ui: { action: '<name>' }.
|
|
400
|
+
const action = e.detail?.action || e.target?.getAttribute?.('action');
|
|
401
|
+
if (!action) return;
|
|
402
|
+
if (action === 'disconnect' || action === 'reauth' || action === 're-authenticate') {
|
|
403
|
+
const emits = action === 'disconnect' ? 'disconnect' : 'reauth';
|
|
404
|
+
this.dispatchEvent(new CustomEvent(emits, {
|
|
405
|
+
bubbles: true,
|
|
406
|
+
detail: { provider: this.provider },
|
|
407
|
+
}));
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://adiaui.dev/a2ui/v0_9/components/IntegrationCard.json",
|
|
4
|
+
"title": "IntegrationCard",
|
|
5
|
+
"description": "Single-tile primitive representing one third-party integration (Slack, GitHub, Stripe, …). Shows the provider logo + name + description + status pill + a single primary action button whose label and variant are derived from `status`. Composes into a grid via the SPEC-063 integrations-page composite. One tile = one provider; the button variant is computed from `status`, not a separate prop. Distinct from <option-card-ui> (a single-select radio with no status / async action) and <card-ui> (the generic bordered surface this specializes).",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"allOf": [
|
|
8
|
+
{
|
|
9
|
+
"$ref": "common_types.json#/$defs/ComponentCommon"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"$ref": "common_types.json#/$defs/CatalogComponentCommon"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"properties": {
|
|
16
|
+
"description": {
|
|
17
|
+
"description": "One-line description of what the integration does.",
|
|
18
|
+
"type": "string",
|
|
19
|
+
"default": ""
|
|
20
|
+
},
|
|
21
|
+
"component": {
|
|
22
|
+
"const": "IntegrationCard"
|
|
23
|
+
},
|
|
24
|
+
"disabled": {
|
|
25
|
+
"description": "Disables the action button.",
|
|
26
|
+
"type": "boolean",
|
|
27
|
+
"default": false
|
|
28
|
+
},
|
|
29
|
+
"error-message": {
|
|
30
|
+
"description": "When `status=\"error\"`, the message to show below the description. Ignored for other statuses.",
|
|
31
|
+
"$ref": "common_types.json#/$defs/DynamicString"
|
|
32
|
+
},
|
|
33
|
+
"logo": {
|
|
34
|
+
"description": "Logo URL (preferred — renders as <img>) or icon name (renders as <icon-ui>). URLs are sniffed by presence of `/`.",
|
|
35
|
+
"type": "string",
|
|
36
|
+
"default": ""
|
|
37
|
+
},
|
|
38
|
+
"name": {
|
|
39
|
+
"description": "Display name shown as the card title. Required.",
|
|
40
|
+
"type": "string",
|
|
41
|
+
"default": ""
|
|
42
|
+
},
|
|
43
|
+
"provider": {
|
|
44
|
+
"description": "Provider key — `slack`, `github`, etc. — used for analytics + a11y label. Required.",
|
|
45
|
+
"type": "string",
|
|
46
|
+
"default": ""
|
|
47
|
+
},
|
|
48
|
+
"status": {
|
|
49
|
+
"description": "Current connection state — drives the button label, variant, and visibility. `available` shows a primary `Connect`; `connected` shows an outline `Configure`; `error` shows an outline `Retry`; `pending` shows a non-interactive spinner; `coming-soon` hides the button.",
|
|
50
|
+
"$ref": "common_types.json#/$defs/DynamicString"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"required": [
|
|
54
|
+
"component",
|
|
55
|
+
"provider",
|
|
56
|
+
"name"
|
|
57
|
+
],
|
|
58
|
+
"unevaluatedProperties": false,
|
|
59
|
+
"x-adiaui": {
|
|
60
|
+
"anti_patterns": [
|
|
61
|
+
{
|
|
62
|
+
"fix": "{\"component\": \"IntegrationCard\", \"provider\": \"slack\",\n \"name\": \"Slack\", \"status\": \"available\"}\n",
|
|
63
|
+
"why": "Manually slotting a Button bypasses the card's status-driven\nbutton semantics. The card renders its own button AND the\nslotted one, producing two actions side-by-side.\n",
|
|
64
|
+
"wrong": "{\"component\": \"IntegrationCard\", \"provider\": \"slack\",\n \"children\": [{\"component\": \"Button\", \"label\": \"Connect\"}]}\n"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"fix": "{\"component\": \"IntegrationCard\", \"provider\": \"slack\",\n \"name\": \"Slack\", \"logo\": \"/slack.svg\", \"status\": \"available\"}\n",
|
|
68
|
+
"why": "Re-implements IntegrationCard with primitives. Loses status\nbadge semantics, accessible-name wiring, and the typed\nconnect/configure events.\n",
|
|
69
|
+
"wrong": "{\"component\": \"Card\", \"children\": [\n {\"component\": \"Image\", \"src\": \"/slack.svg\"},\n {\"component\": \"Text\", \"value\": \"Slack\"},\n {\"component\": \"Button\", \"label\": \"Connect\"}]}\n"
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
"category": "display",
|
|
73
|
+
"composes": [
|
|
74
|
+
"icon-ui",
|
|
75
|
+
"badge-ui",
|
|
76
|
+
"button-ui"
|
|
77
|
+
],
|
|
78
|
+
"events": {
|
|
79
|
+
"configure": {
|
|
80
|
+
"description": "Fired when the user clicks the action button while `status=\"connected\"`.",
|
|
81
|
+
"detail": {
|
|
82
|
+
"provider": {
|
|
83
|
+
"description": "The provider key for this card.",
|
|
84
|
+
"type": "string"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
"connect": {
|
|
89
|
+
"description": "Fired when the user clicks the action button while `status=\"available\"`.",
|
|
90
|
+
"detail": {
|
|
91
|
+
"provider": {
|
|
92
|
+
"description": "The provider key for this card.",
|
|
93
|
+
"type": "string"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"disconnect": {
|
|
98
|
+
"description": "Fired when the consumer wires a `<menu-ui slot=\"actions\">` overflow with a `disconnect` item. The card does not stamp this menu — it bubbles whatever consumer markup dispatches.",
|
|
99
|
+
"detail": {
|
|
100
|
+
"provider": {
|
|
101
|
+
"description": "The provider key for this card.",
|
|
102
|
+
"type": "string"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"retry": {
|
|
107
|
+
"description": "Fired when the user clicks the action button while `status=\"error\"`.",
|
|
108
|
+
"detail": {
|
|
109
|
+
"provider": {
|
|
110
|
+
"description": "The provider key for this card.",
|
|
111
|
+
"type": "string"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"examples": [
|
|
117
|
+
{
|
|
118
|
+
"description": "Available integration with logo, description, and primary Connect button.",
|
|
119
|
+
"a2ui": "{\n \"id\": \"card-slack\",\n \"component\": \"IntegrationCard\",\n \"provider\": \"slack\",\n \"name\": \"Slack\",\n \"logo\": \"/integrations/slack.svg\",\n \"description\": \"Send AI replies and notifications to channels.\",\n \"status\": \"available\"\n}",
|
|
120
|
+
"name": "available"
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"description": "Connected integration with success badge and outline Configure button.",
|
|
124
|
+
"a2ui": "{\n \"id\": \"card-github\",\n \"component\": \"IntegrationCard\",\n \"provider\": \"github\",\n \"name\": \"GitHub\",\n \"logo\": \"/integrations/github.svg\",\n \"description\": \"Sync issues and pull requests.\",\n \"status\": \"connected\"\n}",
|
|
125
|
+
"name": "connected"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"description": "Error integration with danger badge, error message, and Retry button.",
|
|
129
|
+
"a2ui": "{\n \"id\": \"card-stripe\",\n \"component\": \"IntegrationCard\",\n \"provider\": \"stripe\",\n \"name\": \"Stripe\",\n \"description\": \"Charge customers and reconcile invoices.\",\n \"status\": \"error\",\n \"error-message\": \"Token expired — re-authenticate.\"\n}",
|
|
130
|
+
"name": "error"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"description": "Coming-soon integration with no action button.",
|
|
134
|
+
"a2ui": "{\n \"id\": \"card-zapier\",\n \"component\": \"IntegrationCard\",\n \"provider\": \"zapier\",\n \"name\": \"Zapier\",\n \"description\": \"Trigger Zaps from AdiaUI events.\",\n \"status\": \"coming-soon\"\n}",
|
|
135
|
+
"name": "coming-soon"
|
|
136
|
+
}
|
|
137
|
+
],
|
|
138
|
+
"keywords": [
|
|
139
|
+
"integration",
|
|
140
|
+
"connect",
|
|
141
|
+
"provider",
|
|
142
|
+
"slack",
|
|
143
|
+
"github",
|
|
144
|
+
"stripe",
|
|
145
|
+
"oauth",
|
|
146
|
+
"third-party",
|
|
147
|
+
"settings",
|
|
148
|
+
"integrations",
|
|
149
|
+
"card",
|
|
150
|
+
"tile"
|
|
151
|
+
],
|
|
152
|
+
"name": "UIIntegrationCard",
|
|
153
|
+
"related": [
|
|
154
|
+
"card",
|
|
155
|
+
"badge",
|
|
156
|
+
"button",
|
|
157
|
+
"menu"
|
|
158
|
+
],
|
|
159
|
+
"slots": {
|
|
160
|
+
"default": {
|
|
161
|
+
"description": "Optional override for the description. When a non-empty default slot is provided it supersedes the `description` prop."
|
|
162
|
+
},
|
|
163
|
+
"actions": {
|
|
164
|
+
"description": "Optional secondary actions — typically a <menu-ui> overflow carrying `Reauthenticate` / `Disconnect` entries. Collapses when empty."
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
"states": [
|
|
168
|
+
{
|
|
169
|
+
"description": "Default — `[status=\"available\"]`.",
|
|
170
|
+
"name": "idle"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"description": "Ready to connect.",
|
|
174
|
+
"attribute": "status=\"available\"",
|
|
175
|
+
"name": "available"
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"description": "Linked — success-tinted border, check badge, outline `Configure` button.",
|
|
179
|
+
"attribute": "status=\"connected\"",
|
|
180
|
+
"name": "connected"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"description": "Connection failed — danger-tinted border, error message visible, outline `Retry` button.",
|
|
184
|
+
"attribute": "status=\"error\"",
|
|
185
|
+
"name": "error"
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"description": "Awaiting response — button shows spinner, non-interactive.",
|
|
189
|
+
"attribute": "status=\"pending\"",
|
|
190
|
+
"name": "pending"
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"description": "Listed but not yet enabled — opacity reduced, button hidden, \"Coming soon\" badge shown.",
|
|
194
|
+
"attribute": "status=\"coming-soon\"",
|
|
195
|
+
"name": "coming-soon"
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"description": "Non-interactive; dimmed.",
|
|
199
|
+
"attribute": "disabled",
|
|
200
|
+
"name": "disabled"
|
|
201
|
+
}
|
|
202
|
+
],
|
|
203
|
+
"status": "stable",
|
|
204
|
+
"synonyms": {
|
|
205
|
+
"connect": [
|
|
206
|
+
"integration",
|
|
207
|
+
"oauth",
|
|
208
|
+
"connection"
|
|
209
|
+
],
|
|
210
|
+
"integration": [
|
|
211
|
+
"integration",
|
|
212
|
+
"connect",
|
|
213
|
+
"provider",
|
|
214
|
+
"third-party"
|
|
215
|
+
],
|
|
216
|
+
"oauth": [
|
|
217
|
+
"integration",
|
|
218
|
+
"connect",
|
|
219
|
+
"auth"
|
|
220
|
+
],
|
|
221
|
+
"provider": [
|
|
222
|
+
"integration",
|
|
223
|
+
"third-party",
|
|
224
|
+
"service"
|
|
225
|
+
],
|
|
226
|
+
"settings": [
|
|
227
|
+
"settings",
|
|
228
|
+
"integration",
|
|
229
|
+
"account"
|
|
230
|
+
]
|
|
231
|
+
},
|
|
232
|
+
"tag": "integration-card-ui",
|
|
233
|
+
"tokens": {
|
|
234
|
+
"--integration-card-bg": {
|
|
235
|
+
"description": "Card background. Defaults to `--a-bg`."
|
|
236
|
+
},
|
|
237
|
+
"--integration-card-border": {
|
|
238
|
+
"description": "Card border color. Defaults to `--a-border`."
|
|
239
|
+
},
|
|
240
|
+
"--integration-card-description-fg": {
|
|
241
|
+
"description": "Description text color."
|
|
242
|
+
},
|
|
243
|
+
"--integration-card-error-fg": {
|
|
244
|
+
"description": "Error-message text color (used when `status=\"error\"`)."
|
|
245
|
+
},
|
|
246
|
+
"--integration-card-gap": {
|
|
247
|
+
"description": "Vertical rhythm between header / body / footer."
|
|
248
|
+
},
|
|
249
|
+
"--integration-card-heading-fg": {
|
|
250
|
+
"description": "Provider name color."
|
|
251
|
+
},
|
|
252
|
+
"--integration-card-logo-size": {
|
|
253
|
+
"description": "Logo square dimension. Defaults to `--a-space-7`."
|
|
254
|
+
},
|
|
255
|
+
"--integration-card-px": {
|
|
256
|
+
"description": "Horizontal padding. Defaults to `--a-space-4`."
|
|
257
|
+
},
|
|
258
|
+
"--integration-card-py": {
|
|
259
|
+
"description": "Vertical padding. Defaults to `--a-space-4`."
|
|
260
|
+
},
|
|
261
|
+
"--integration-card-radius": {
|
|
262
|
+
"description": "Card corner radius. Defaults to `--a-radius-md`."
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
"traits": [],
|
|
266
|
+
"version": 1
|
|
267
|
+
}
|
|
268
|
+
}
|