@cobdfamily/clf-core 7.0.1

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 (44) hide show
  1. package/README.md +250 -0
  2. package/dist/_variables.scss +973 -0
  3. package/dist/components/cobd-embed.d.ts +4 -0
  4. package/dist/components/cobd-embed.js +163 -0
  5. package/dist/components/cobd-nav.d.ts +1 -0
  6. package/dist/components/cobd-nav.js +383 -0
  7. package/dist/components/cobd-support.d.ts +26 -0
  8. package/dist/components/cobd-support.js +296 -0
  9. package/dist/components/font-scale-toggle.d.ts +9 -0
  10. package/dist/components/font-scale-toggle.js +159 -0
  11. package/dist/components/forms/checkbox.d.ts +1 -0
  12. package/dist/components/forms/checkbox.js +118 -0
  13. package/dist/components/forms/common.d.ts +17 -0
  14. package/dist/components/forms/common.js +137 -0
  15. package/dist/components/forms/index.d.ts +5 -0
  16. package/dist/components/forms/index.js +13 -0
  17. package/dist/components/forms/select.d.ts +1 -0
  18. package/dist/components/forms/select.js +132 -0
  19. package/dist/components/forms/submit.d.ts +1 -0
  20. package/dist/components/forms/submit.js +78 -0
  21. package/dist/components/forms/textarea.d.ts +1 -0
  22. package/dist/components/forms/textarea.js +95 -0
  23. package/dist/components/forms/textfield.d.ts +1 -0
  24. package/dist/components/forms/textfield.js +125 -0
  25. package/dist/components/index.d.ts +7 -0
  26. package/dist/components/index.js +33 -0
  27. package/dist/components/theme-toggle.d.ts +10 -0
  28. package/dist/components/theme-toggle.js +130 -0
  29. package/dist/i18n/chrome.json +94 -0
  30. package/dist/navs/cobd.ca.json +83 -0
  31. package/dist/navs/more-cobd.json +60 -0
  32. package/dist/navs.d.ts +27 -0
  33. package/dist/navs.js +193 -0
  34. package/dist/theming/font-scale-paint.d.ts +0 -0
  35. package/dist/theming/font-scale-paint.js +32 -0
  36. package/dist/theming/init.d.ts +1 -0
  37. package/dist/theming/init.js +19 -0
  38. package/dist/theming/runtime.d.ts +22 -0
  39. package/dist/theming/runtime.js +333 -0
  40. package/dist/tokens.css +972 -0
  41. package/dist/tokens.json +97 -0
  42. package/dist/tokens.scss +95 -0
  43. package/package.json +123 -0
  44. package/src/navs/schema.json +64 -0
@@ -0,0 +1,4 @@
1
+ declare class CobdEmbedElement extends HTMLElement {
2
+ connectedCallback(): void;
3
+ }
4
+ export { CobdEmbedElement };
@@ -0,0 +1,163 @@
1
+ // <cobd-embed [service="https://..."]>
2
+ // <a href="https://target.example/...">
3
+ // Optional link text -- used as the iframe title
4
+ // and as the screen-reader fallback when JS is
5
+ // off.
6
+ // </a>
7
+ // </cobd-embed>
8
+ //
9
+ // Progressive-enhancement embed. Authoring shape
10
+ // is just an anchor; on connect, the element
11
+ // upgrades to a sandboxed iframe pointing at the
12
+ // embed microservice at embed.openapis.ca/v/embed.
13
+ //
14
+ // Why the proxy: provider embeds (YouTube IFrame
15
+ // API, Twitter widgets.js, Bluesky, etc.) ship
16
+ // inline scripts and load third-party resources.
17
+ // Inlining them on the consumer page would force
18
+ // every consumer CSP to whitelist every provider
19
+ // origin. Instead, the proxy returns a self-
20
+ // contained HTML doc; we iframe that doc; the
21
+ // provider scripts run inside embed.openapis.ca's
22
+ // origin and never touch the consumer's CSP.
23
+ //
24
+ // Light-DOM only. No shadow root, no slot routing.
25
+ // The anchor stays in the DOM (visible below the
26
+ // iframe by default) so screen-reader users can
27
+ // always escape to the real URL -- some embeds
28
+ // trap focus inside provider iframes badly, and
29
+ // the explicit click-through is the seatbelt.
30
+ //
31
+ // Set the `service=` attribute to override the
32
+ // default proxy URL (e.g. for COBD-internal staging
33
+ // against a different embed deployment). Defaults
34
+ // to https://embed.openapis.ca/v/embed.
35
+ const DEFAULT_SERVICE = "https://embed.openapis.ca/v1/embed";
36
+ const STYLE_ID = "cobd-embed-base-style";
37
+ const BASE_STYLE = `
38
+ cobd-embed {
39
+ display: block;
40
+ position: relative;
41
+ width: 100%;
42
+ aspect-ratio: 16 / 9;
43
+ }
44
+ cobd-embed iframe {
45
+ width: 100%;
46
+ height: 100%;
47
+ border: 0;
48
+ display: block;
49
+ }
50
+ /* Hide the original anchor visually once the
51
+ iframe is in place, but keep it reachable in
52
+ the DOM (and to assistive tech via the
53
+ .cobd-embed-fallback link rendered below the
54
+ iframe). The original anchor is moved out of
55
+ tab order via tabindex=-1 in TS; this CSS
56
+ just hides it visually. */
57
+ cobd-embed > a[hidden] {
58
+ display: none !important;
59
+ }
60
+ .cobd-embed-fallback {
61
+ display: block;
62
+ margin-top: var(--cobd-spacing-xs, 4px);
63
+ font-size: 0.875rem;
64
+ color: var(--cobd-color-link, #3a6fd1);
65
+ }
66
+ .cobd-embed-fallback:focus-visible {
67
+ outline: 2px solid currentColor;
68
+ outline-offset: 2px;
69
+ }
70
+ `;
71
+ function ensureBaseStyle() {
72
+ if (typeof document === "undefined")
73
+ return;
74
+ if (document.getElementById(STYLE_ID))
75
+ return;
76
+ const style = document.createElement("style");
77
+ style.id = STYLE_ID;
78
+ style.textContent = BASE_STYLE;
79
+ document.head.appendChild(style);
80
+ }
81
+ // Hostname-only display string for the fallback
82
+ // link's "Open at <host>". Using URL() means we
83
+ // don't need a regex and get the parser's
84
+ // normalisation for free (lowercased host, IDN
85
+ // handling). Falls back to the raw URL string if
86
+ // parsing throws -- defensive against an authoring
87
+ // typo. We never throw out of connectedCallback.
88
+ function hostOf(url) {
89
+ try {
90
+ return new URL(url).hostname;
91
+ }
92
+ catch {
93
+ return url;
94
+ }
95
+ }
96
+ class CobdEmbedElement extends HTMLElement {
97
+ connectedCallback() {
98
+ ensureBaseStyle();
99
+ // Bail out idempotently if we've already
100
+ // upgraded -- connectedCallback can fire
101
+ // again when the element moves in the DOM.
102
+ if (this.querySelector("iframe"))
103
+ return;
104
+ const anchor = this.querySelector("a[href]");
105
+ if (!anchor) {
106
+ // No authoring content to upgrade. Leave
107
+ // the element's contents alone so the
108
+ // author can iterate without a flash.
109
+ return;
110
+ }
111
+ const href = anchor.href;
112
+ if (!href)
113
+ return;
114
+ const title = (anchor.textContent || "").trim()
115
+ || hostOf(href);
116
+ const service = this.getAttribute("service")
117
+ || DEFAULT_SERVICE;
118
+ const iframe = document.createElement("iframe");
119
+ iframe.src = service + "?url="
120
+ + encodeURIComponent(href);
121
+ iframe.title = title;
122
+ iframe.loading = "lazy";
123
+ iframe.referrerPolicy = "no-referrer";
124
+ // No allow-same-origin: the proxy origin gets
125
+ // a unique opaque origin, so its scripts can
126
+ // still run (allow-scripts) and load child
127
+ // iframes for the provider, but can't act on
128
+ // cookies for embed.openapis.ca. Inner
129
+ // provider iframes have their own origin
130
+ // regardless.
131
+ iframe.setAttribute("sandbox", "allow-scripts allow-popups "
132
+ + "allow-popups-to-escape-sandbox");
133
+ // `allow` is the modern feature-policy
134
+ // surface; opt fullscreen in so video
135
+ // providers can present their player
136
+ // controls. Audio/encrypted-media are
137
+ // commonly needed for SoundCloud / Spotify.
138
+ iframe.setAttribute("allow", "fullscreen; encrypted-media; "
139
+ + "picture-in-picture");
140
+ // Hide the original anchor and pull it out
141
+ // of tab order. We render a parallel
142
+ // fallback link AFTER the iframe so the
143
+ // user still has a deterministic "open the
144
+ // real URL" anchor without competing for
145
+ // focus with the iframe's contents.
146
+ anchor.setAttribute("hidden", "");
147
+ anchor.setAttribute("tabindex", "-1");
148
+ const fallback = document.createElement("a");
149
+ fallback.className = "cobd-embed-fallback";
150
+ fallback.href = href;
151
+ fallback.target = "_blank";
152
+ fallback.rel = "noopener nofollow";
153
+ fallback.referrerPolicy = "no-referrer";
154
+ fallback.textContent = "Open at " + hostOf(href);
155
+ this.appendChild(iframe);
156
+ this.appendChild(fallback);
157
+ }
158
+ }
159
+ if (typeof customElements !== "undefined"
160
+ && !customElements.get("cobd-embed")) {
161
+ customElements.define("cobd-embed", CobdEmbedElement);
162
+ }
163
+ export { CobdEmbedElement };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,383 @@
1
+ // <cobd-nav path="..." aria-label="..." lines="inset">
2
+ // -- Light-DOM custom element that renders one of the
3
+ // COBD nav surfaces.
4
+ //
5
+ // `path` accepts three shapes:
6
+ //
7
+ // path="local"
8
+ // Read the element's own <ul>/<li> children as the
9
+ // nav source. Server-rendered HTML works unchanged
10
+ // without JS. Locale labels are not supported in
11
+ // this mode -- the markup is the truth.
12
+ //
13
+ // path="<URL>" (has a scheme or leading /)
14
+ // Fetch the JSON from that URL and render it.
15
+ //
16
+ // path="<slug>" (anything else)
17
+ // Look the slug up via getNav() from
18
+ // clf-core's bundled nav data.
19
+ //
20
+ // Render shape depends on whether Ionic is in use on
21
+ // the page. We detect this by looking for <ion-app>
22
+ // in the DOM at render time (<ion-app> is present in
23
+ // the DOM before <cobd-nav> upgrades in every Ionic
24
+ // page, vanilla or Angular, which sidesteps the
25
+ // custom-elements-not-yet-defined race that a
26
+ // customElements.get("ion-list") check would hit
27
+ // under Angular Ionic's deferred bootstrap):
28
+ //
29
+ // Ionic present -> emit <ion-list> of <ion-item>s
30
+ // (or transform the local <ul>
31
+ // into the same).
32
+ // Ionic absent -> emit a plain <ul> of <li><a>s
33
+ // in slug + URL modes; in local
34
+ // mode the existing <ul> is the
35
+ // source of truth, so the
36
+ // element leaves it alone and
37
+ // just tags the active link.
38
+ //
39
+ // Light DOM (no shadow root) so a surrounding
40
+ // <ion-accordion slot="content"> still slot-routes
41
+ // the rendered list, and so ion-* tags inside the
42
+ // element get upgraded by the page's Ionic instance.
43
+ //
44
+ // Static-HTML fallback: in slug + URL modes the host
45
+ // renders skeleton ion-items (or whatever) as child
46
+ // content; those stay visible until the element
47
+ // upgrades and replaces them. In local mode the
48
+ // <ul>/<li> tree IS the source of truth -- a
49
+ // JS-blocked page sees the same data, just as a
50
+ // plain anchor list.
51
+ // @ts-expect-error -- ../navs.js is generated by
52
+ // build-navs.mjs into dist/ and has no source
53
+ // counterpart in src/ at type-check time. At
54
+ // runtime cobd-nav.js lives at
55
+ // dist/components/cobd-nav.js and navs.js lives
56
+ // at dist/navs.js, so the relative import is one
57
+ // directory up.
58
+ import { getNav } from "../navs.js";
59
+ // `path="local"` is the sentinel that means "read
60
+ // the children." URL form requires either an
61
+ // http(s):// scheme or a leading `/` so a slug
62
+ // like `cobd.ca` is not misread as a hostname.
63
+ function resolveMode(path) {
64
+ if (path === "local")
65
+ return "local";
66
+ if (/^https?:\/\//i.test(path))
67
+ return "url";
68
+ if (path.startsWith("/"))
69
+ return "url";
70
+ return "slug";
71
+ }
72
+ // Sidestep the customElements-not-yet-defined race
73
+ // under Angular by looking for <ion-app> in the DOM
74
+ // rather than checking customElements.get("ion-list").
75
+ // <ion-app> is present pre-bootstrap; ion-list isn't.
76
+ function ionicPresent() {
77
+ return typeof document !== "undefined"
78
+ && document.querySelector("ion-app") !== null;
79
+ }
80
+ // Normalise a URL string (root-relative, absolute,
81
+ // or with/without trailing slash) so the active-page
82
+ // comparison works regardless of how the nav author
83
+ // wrote the href.
84
+ function norm(href) {
85
+ try {
86
+ const u = new URL(href, location.origin);
87
+ const p = u.pathname.replace(/\/$/, "") || "/";
88
+ return u.origin + p;
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ }
94
+ // ------------------------------------------------------------
95
+ // Local-mode parser: <ul>/<li> -> NavItem[]
96
+ //
97
+ // <li>
98
+ // <ion-icon name="..."></ion-icon>? -- optional
99
+ // <a href="..." [target="_blank"]>Label</a> -- optional;
100
+ // group-only
101
+ // items omit
102
+ // <ul>...</ul> -- optional
103
+ // submenu
104
+ // </li>
105
+ //
106
+ // Items without either an <a> or a textual label are
107
+ // skipped. Author-supplied DOM is treated as the
108
+ // source of truth; we do not validate against the
109
+ // JSON nav schema (different shape, different
110
+ // tradeoffs).
111
+ //
112
+ // Only used when Ionic is present -- the parsed
113
+ // items feed buildIonList. In Ionic-absent mode we
114
+ // leave the <ul>/<li> tree alone entirely.
115
+ // ------------------------------------------------------------
116
+ function parseLocalUL(ul) {
117
+ const out = [];
118
+ for (const li of Array.from(ul.children)) {
119
+ if (li.tagName !== "LI")
120
+ continue;
121
+ const a = li.querySelector(":scope > a");
122
+ const subUl = li.querySelector(":scope > ul");
123
+ const ionIcon = li.querySelector(":scope > ion-icon, :scope > a > ion-icon");
124
+ let label = "";
125
+ if (a) {
126
+ const clone = a.cloneNode(true);
127
+ for (const ic of Array.from(clone.querySelectorAll("ion-icon"))) {
128
+ ic.remove();
129
+ }
130
+ label = clone.textContent?.trim() ?? "";
131
+ }
132
+ else {
133
+ for (const node of Array.from(li.childNodes)) {
134
+ if (node.nodeType === Node.TEXT_NODE) {
135
+ const t = node.textContent?.trim() ?? "";
136
+ if (t) {
137
+ label = t;
138
+ break;
139
+ }
140
+ }
141
+ }
142
+ }
143
+ if (!label)
144
+ continue;
145
+ const item = { label };
146
+ if (a?.getAttribute("href")) {
147
+ item.href = a.getAttribute("href") ?? undefined;
148
+ }
149
+ if (a?.getAttribute("target") === "_blank") {
150
+ item.rel = "external";
151
+ }
152
+ if (ionIcon) {
153
+ const name = ionIcon.getAttribute("name");
154
+ if (name)
155
+ item.icon = name;
156
+ }
157
+ if (subUl) {
158
+ const kids = parseLocalUL(subUl);
159
+ if (kids.length)
160
+ item.items = kids;
161
+ }
162
+ out.push(item);
163
+ }
164
+ return out;
165
+ }
166
+ // ------------------------------------------------------------
167
+ // Renderer: NavItem[] -> <ion-list> (Ionic-present)
168
+ // ------------------------------------------------------------
169
+ function buildIonList(items, lines, ariaLabel, here) {
170
+ const list = document.createElement("ion-list");
171
+ list.setAttribute("lines", lines);
172
+ if (ariaLabel)
173
+ list.setAttribute("aria-label", ariaLabel);
174
+ for (const item of items) {
175
+ const it = document.createElement("ion-item");
176
+ if (item.href)
177
+ it.setAttribute("href", item.href);
178
+ it.setAttribute("button", "");
179
+ it.setAttribute("detail", "true");
180
+ if (item.rel === "external") {
181
+ it.setAttribute("target", "_blank");
182
+ }
183
+ if (item.href && norm(item.href) === here) {
184
+ it.setAttribute("aria-current", "page");
185
+ it.classList.add("cobd-active-nav-item");
186
+ }
187
+ if (item.icon) {
188
+ const ic = document.createElement("ion-icon");
189
+ ic.setAttribute("slot", "start");
190
+ ic.setAttribute("name", item.icon);
191
+ ic.setAttribute("aria-hidden", "true");
192
+ it.appendChild(ic);
193
+ }
194
+ const lbl = document.createElement("ion-label");
195
+ lbl.textContent = item.label;
196
+ it.appendChild(lbl);
197
+ list.appendChild(it);
198
+ }
199
+ return list;
200
+ }
201
+ // ------------------------------------------------------------
202
+ // Renderer: NavItem[] -> <ul> (Ionic-absent)
203
+ //
204
+ // Plain semantic markup. <ion-icon> tags still emit;
205
+ // they stay as inert unupgraded custom elements when
206
+ // Ionic isn't on the page (zero-width text), and
207
+ // consumers who DO bring ionicons separately get the
208
+ // glyphs for free. Active link gets aria-current +
209
+ // the .cobd-active-nav-item class on the <li>.
210
+ // ------------------------------------------------------------
211
+ function buildPlainUL(items, ariaLabel, here) {
212
+ const ul = document.createElement("ul");
213
+ if (ariaLabel)
214
+ ul.setAttribute("aria-label", ariaLabel);
215
+ for (const item of items) {
216
+ const li = document.createElement("li");
217
+ const isActive = !!item.href
218
+ && norm(item.href) === here;
219
+ if (isActive)
220
+ li.classList.add("cobd-active-nav-item");
221
+ if (item.href) {
222
+ const a = document.createElement("a");
223
+ a.setAttribute("href", item.href);
224
+ if (item.rel === "external") {
225
+ a.setAttribute("target", "_blank");
226
+ a.setAttribute("rel", "noopener noreferrer");
227
+ }
228
+ if (isActive)
229
+ a.setAttribute("aria-current", "page");
230
+ if (item.icon) {
231
+ const ic = document.createElement("ion-icon");
232
+ ic.setAttribute("name", item.icon);
233
+ ic.setAttribute("aria-hidden", "true");
234
+ a.appendChild(ic);
235
+ }
236
+ a.appendChild(document.createTextNode(item.label));
237
+ li.appendChild(a);
238
+ }
239
+ else {
240
+ li.appendChild(document.createTextNode(item.label));
241
+ }
242
+ if (item.items?.length) {
243
+ li.appendChild(buildPlainUL(item.items, null, here));
244
+ }
245
+ ul.appendChild(li);
246
+ }
247
+ return ul;
248
+ }
249
+ // ------------------------------------------------------------
250
+ // In-place active marker for local + !ionic
251
+ //
252
+ // The author's <ul>/<li> tree IS the rendered output;
253
+ // we just walk the existing anchors and stamp
254
+ // aria-current="page" on whichever matches the
255
+ // current page. Idempotent so re-renders are safe.
256
+ // ------------------------------------------------------------
257
+ function markActiveInPlace(root, here) {
258
+ for (const a of Array.from(root.querySelectorAll("a"))) {
259
+ const href = a.getAttribute("href");
260
+ const wasActive = a.getAttribute("aria-current") === "page";
261
+ const isActive = !!href && norm(href) === here;
262
+ if (isActive) {
263
+ a.setAttribute("aria-current", "page");
264
+ a.closest("li")?.classList.add("cobd-active-nav-item");
265
+ }
266
+ else if (wasActive) {
267
+ a.removeAttribute("aria-current");
268
+ a.closest("li")?.classList.remove("cobd-active-nav-item");
269
+ }
270
+ }
271
+ }
272
+ // Light-DOM elements get `display: inline` by
273
+ // default, which would collapse the contained list
274
+ // to zero height. Inject one global rule the first
275
+ // time the element upgrades; idempotent so each
276
+ // page only pays for it once.
277
+ function ensureBaseStyle() {
278
+ if (typeof document === "undefined")
279
+ return;
280
+ const id = "cobd-nav-base-style";
281
+ if (document.getElementById(id))
282
+ return;
283
+ const style = document.createElement("style");
284
+ style.id = id;
285
+ style.textContent = "cobd-nav { display: block; }";
286
+ document.head.appendChild(style);
287
+ }
288
+ class CobdNav extends HTMLElement {
289
+ constructor() {
290
+ super(...arguments);
291
+ this.rendered = false;
292
+ }
293
+ static get observedAttributes() {
294
+ return ["path", "lines", "aria-label", "locale"];
295
+ }
296
+ connectedCallback() {
297
+ ensureBaseStyle();
298
+ if (!this.rendered)
299
+ this.render();
300
+ }
301
+ attributeChangedCallback() {
302
+ if (this.isConnected && this.rendered)
303
+ this.render();
304
+ }
305
+ async render() {
306
+ const path = this.getAttribute("path");
307
+ if (!path)
308
+ return;
309
+ const mode = resolveMode(path);
310
+ const lines = this.getAttribute("lines") ?? "inset";
311
+ const ariaLabel = this.getAttribute("aria-label");
312
+ const locale = this.getAttribute("locale") ?? undefined;
313
+ const useIonic = ionicPresent();
314
+ const here = norm(location.href);
315
+ // Local + !ionic: the markup IS the rendered
316
+ // output. Just tag active links and exit.
317
+ if (mode === "local" && !useIonic) {
318
+ markActiveInPlace(this, here);
319
+ this.rendered = true;
320
+ return;
321
+ }
322
+ let items = null;
323
+ try {
324
+ if (mode === "local") {
325
+ const ul = this.querySelector(":scope > ul");
326
+ if (!ul) {
327
+ console.error("cobd-nav: path=\"local\""
328
+ + " but no child <ul> found");
329
+ return;
330
+ }
331
+ items = parseLocalUL(ul);
332
+ }
333
+ else if (mode === "url") {
334
+ const res = await fetch(path, {
335
+ cache: "no-cache"
336
+ });
337
+ if (!res.ok) {
338
+ throw new Error("http " + res.status);
339
+ }
340
+ const nav = await res.json();
341
+ items = applyLocale(nav.items, locale);
342
+ }
343
+ else {
344
+ const nav = getNav(path, locale);
345
+ items = nav.items;
346
+ }
347
+ }
348
+ catch (err) {
349
+ console.error("cobd-nav:", err);
350
+ return;
351
+ }
352
+ if (!items)
353
+ return;
354
+ const tree = useIonic
355
+ ? buildIonList(items, lines, ariaLabel, here)
356
+ : buildPlainUL(items, ariaLabel, here);
357
+ this.replaceChildren(tree);
358
+ this.rendered = true;
359
+ }
360
+ }
361
+ // Apply label_i18n overrides for URL mode. getNav()
362
+ // already does this internally; here we apply the
363
+ // same shape to remote JSON before render so the
364
+ // two paths behave the same.
365
+ function applyLocale(items, locale) {
366
+ if (!locale)
367
+ return items;
368
+ return items.map(it => {
369
+ const i18n = it.label_i18n;
370
+ const next = { ...it };
371
+ delete next.label_i18n;
372
+ if (i18n && i18n[locale]) {
373
+ next.label = i18n[locale];
374
+ }
375
+ if (next.items) {
376
+ next.items = applyLocale(next.items, locale);
377
+ }
378
+ return next;
379
+ });
380
+ }
381
+ if (!customElements.get("cobd-nav")) {
382
+ customElements.define("cobd-nav", CobdNav);
383
+ }
@@ -0,0 +1,26 @@
1
+ interface SupportAction {
2
+ label: string;
3
+ icon?: string;
4
+ href?: string;
5
+ target?: string;
6
+ share?: boolean;
7
+ }
8
+ declare function ionicPresent(): boolean;
9
+ declare const BASE_STYLE_ID = "cobd-support-base-style";
10
+ declare const BASE_STYLE = "\ncobd-support { display: inline-block; }\n.cobd-support-menu {\n border: 1px solid var(--cobd-color-medium, #92949c);\n border-radius: var(--cobd-radius-md, 8px);\n padding: var(--cobd-spacing-xs, 4px);\n background: var(--cobd-color-background, #fff);\n color: var(--cobd-color-foreground, #1a1a1a);\n box-shadow: 0 4px 14px rgba(0, 0, 0, 0.15);\n min-width: 14rem;\n margin: 0;\n}\n.cobd-support-menu-item {\n display: flex;\n align-items: center;\n gap: var(--cobd-spacing-sm, 8px);\n padding: var(--cobd-spacing-sm, 8px) var(--cobd-spacing-md, 16px);\n color: inherit;\n background: none;\n border: 0;\n border-radius: var(--cobd-radius-sm, 4px);\n text-decoration: none;\n font: inherit;\n text-align: left;\n width: 100%;\n cursor: pointer;\n}\n.cobd-support-menu-item:hover,\n.cobd-support-menu-item:focus-visible {\n background: var(--cobd-color-light, #f4f5f8);\n outline: 2px solid var(--cobd-color-primary, #72cadb);\n outline-offset: -2px;\n}\n.cobd-support-button {\n font-family: var(--cobd-typography-family-sans,\n system-ui, sans-serif);\n font-size: var(--cobd-typography-size-md, 1rem);\n font-weight: var(--cobd-typography-weight-medium, 500);\n padding: var(--cobd-spacing-sm, 8px) var(--cobd-spacing-md, 16px);\n border-radius: var(--cobd-radius-sm, 4px);\n border: 0;\n background: var(--cobd-color-primary, #72cadb);\n color: var(--cobd-color-primary-contrast, #000);\n cursor: pointer;\n}\n.cobd-support-button:hover {\n background: var(--cobd-color-primary-strong,\n var(--cobd-color-primary, #72cadb));\n}\n";
11
+ declare function ensureBaseStyle(): void;
12
+ declare function readCustomActions(host: HTMLElement): SupportAction[] | null;
13
+ declare class CobdSupport extends HTMLElement {
14
+ static get observedAttributes(): string[];
15
+ private rendered;
16
+ private customActions;
17
+ connectedCallback(): void;
18
+ attributeChangedCallback(): void;
19
+ private render;
20
+ private defaultActions;
21
+ private renderIonic;
22
+ private openIonicSheet;
23
+ private renderPlain;
24
+ private buildPlainItem;
25
+ private handleAction;
26
+ }