@flung.dev/connect 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -69,6 +69,7 @@ export function Accounts() {
69
69
  | `platforms` | | Offer only these, comma-separated. A token made with `platforms` is narrowed to those as well. |
70
70
  | `mode` | | `popup` (the default) opens a small window. `redirect` sends the whole page to the platform and back, for pages that cannot open windows; Flung returns to the same address, and the panel reports the result and tidies the address. |
71
71
  | `theme` | | `light` or `dark`. Follows the page's scheme when left out. |
72
+ | `branding` | | `none` hides the small "Secured by Flung" line under the list. |
72
73
  | | `labels` | Every word the panel shows, to put it in another language. See `DEFAULT_LABELS`. |
73
74
  | | `refresh()` | Reads the accounts again. |
74
75
 
@@ -85,7 +86,7 @@ Events bubble and cross the shadow root.
85
86
 
86
87
  ## Looks
87
88
 
88
- The panel draws in a shadow root. Set any of `--flung-font`, `--flung-text`, `--flung-muted`, `--flung-surface`, `--flung-raised`, `--flung-border`, `--flung-accent`, `--flung-accent-text`, `--flung-danger`, `--flung-warning` and `--flung-radius` on the element, or style its parts with `::part()`: `panel`, `list`, `platform`, `badge`, `name`, `account`, `status`, `form`, `note`, `confirm`, `message`.
89
+ The panel draws in a shadow root. Set any of `--flung-font`, `--flung-text`, `--flung-muted`, `--flung-surface`, `--flung-raised`, `--flung-border`, `--flung-accent`, `--flung-accent-text`, `--flung-danger`, `--flung-warning`, `--flung-success`, `--flung-mark-strength` (how much of each platform's brand colour its mark keeps) and `--flung-radius` on the element, or style its parts with `::part()`: `panel`, `list`, `platform`, `badge`, `name`, `account`, `status`, `form`, `note`, `confirm`, `message`, `footer`.
89
90
 
90
91
  ```css
91
92
  flung-connect { --flung-accent: #0f766e; --flung-radius: 12px; }
@@ -120,7 +121,7 @@ button.onclick = async () => {
120
121
  | Path | Purpose |
121
122
  |---|---|
122
123
  | `src/core/` | `ConnectClient`, the popup and redirect flows, platform names, and the API shapes it reads. No DOM needed except `window.open` in the popup flow. |
123
- | `src/element/` | `<flung-connect>`: the element, its styles, words, events and node helpers. |
124
+ | `src/element/` | `<flung-connect>`: the element, its styles, words, events and node helpers. `marks.ts` is the platforms' marks (Simple Icons; Boxicons for LinkedIn) and Flung's, written by `pnpm sync:icons`. |
124
125
  | `src/react/` | `<FlungConnect>`, the element for React. |
125
126
  | `src/register.ts` | The side-effect entry that registers the element. |
126
127
  | `src/__tests__/` | The client, flows, element and React component against a fake of the seven calls (`fake-api.ts`), in jsdom. |
@@ -24,7 +24,9 @@ export declare class FlungConnectError extends Error {
24
24
  export declare class ConnectClient {
25
25
  #private;
26
26
  constructor(options: ConnectClientOptions);
27
+ /** Every platform, and whether this Flung can connect it. */
27
28
  platforms(): Promise<PlatformInfo[]>;
29
+ /** The connected accounts the token may see: its profile's, or the whole workspace's. */
28
30
  accounts(): Promise<Account[]>;
29
31
  /** Starts connecting a new account. `handle` is the Bluesky handle, or the Mastodon instance (required there). */
30
32
  connect(platform: Platform, options?: {
@@ -37,6 +39,7 @@ export declare class ConnectClient {
37
39
  }): Promise<ConnectSession>;
38
40
  /** Deletes the account's stored tokens. It stays listed as `disconnected`. */
39
41
  disconnect(accountId: string): Promise<Account>;
42
+ /** A connect session's state: pending, connected, failed or expired. */
40
43
  session(id: string): Promise<ConnectSession>;
41
44
  /** Resolves once the session is no longer `pending`. Rejects with an `AbortError` when `signal` aborts. */
42
45
  waitForSession(id: string, options?: {
@@ -28,9 +28,11 @@ export class ConnectClient {
28
28
  this.#source = options.token;
29
29
  this.#token = typeof options.token === 'string' ? options.token : null;
30
30
  }
31
+ /** Every platform, and whether this Flung can connect it. */
31
32
  async platforms() {
32
33
  return (await this.#call('GET', '/v1/platforms')).data;
33
34
  }
35
+ /** The connected accounts the token may see: its profile's, or the whole workspace's. */
34
36
  async accounts() {
35
37
  return (await this.#call('GET', '/v1/accounts')).data;
36
38
  }
@@ -46,6 +48,7 @@ export class ConnectClient {
46
48
  async disconnect(accountId) {
47
49
  return this.#call('DELETE', `/v1/accounts/${encodeURIComponent(accountId)}`);
48
50
  }
51
+ /** A connect session's state: pending, connected, failed or expired. */
49
52
  async session(id) {
50
53
  return this.#call('GET', `/v1/connect-sessions/${encodeURIComponent(id)}`);
51
54
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Every word the panel shows, in English. Pass `labels` to put it in another language; `{platform}`, `{account}`,
3
- * `{message}` and `{date}` are filled in where a label has them.
3
+ * `{message}`, `{date}` and `{count}` are filled in where a label has them.
4
4
  */
5
5
  export interface ConnectLabels {
6
6
  /** The panel's name for a screen reader. */
@@ -24,6 +24,12 @@ export interface ConnectLabels {
24
24
  disconnected: string;
25
25
  needsReconnect: string;
26
26
  reconnectBy: string;
27
+ /** Under a platform's name once it has accounts. */
28
+ connectedCount: string;
29
+ /** Under a platform's name before it has any. */
30
+ notConnected: string;
31
+ /** Before Flung's name in the panel's footer, which `branding="none"` hides. */
32
+ securedBy: string;
27
33
  loading: string;
28
34
  loadFailed: string;
29
35
  empty: string;
@@ -19,6 +19,9 @@ export const DEFAULT_LABELS = {
19
19
  disconnected: '{account} is disconnected.',
20
20
  needsReconnect: 'Needs reconnecting',
21
21
  reconnectBy: 'Reconnect by {date}',
22
+ connectedCount: '{count} connected',
23
+ notConnected: 'Not connected',
24
+ securedBy: 'Secured by',
22
25
  loading: 'Loading accounts…',
23
26
  loadFailed: 'Could not load accounts: {message}',
24
27
  empty: 'There is nothing to connect here.',
@@ -13,7 +13,11 @@ export declare function displayName(account: Account): string;
13
13
  export declare function button(text: string, key: string, onClick: () => void, options?: {
14
14
  primary?: boolean;
15
15
  danger?: boolean;
16
+ warning?: boolean;
17
+ quiet?: boolean;
16
18
  disabled?: boolean;
17
19
  label?: string;
18
20
  }): HTMLButtonElement;
21
+ /** An icon drawn from one path, for the platform marks and Flung's own. Hidden from screen readers: the name beside it says what it is. */
22
+ export declare function mark(path: string, viewBox?: string, className?: string): SVGSVGElement;
19
23
  export {};
@@ -22,8 +22,20 @@ export function displayName(account) {
22
22
  }
23
23
  /** A button that calls `onClick`, with a `data-key` so focus can be put back on it after the panel is drawn again. */
24
24
  export function button(text, key, onClick, options = {}) {
25
- const className = options.primary === true ? 'primary' : options.danger === true ? 'danger' : undefined;
25
+ const className = [options.primary === true && 'primary', options.danger === true && 'danger', options.warning === true && 'warning', options.quiet === true && 'quiet'].filter(Boolean).join(' ') || undefined;
26
26
  const element = html('button', { type: 'button', class: className, 'data-key': key, disabled: options.disabled === true, 'aria-label': options.label }, text);
27
27
  element.addEventListener('click', onClick);
28
28
  return element;
29
29
  }
30
+ /** An icon drawn from one path, for the platform marks and Flung's own. Hidden from screen readers: the name beside it says what it is. */
31
+ export function mark(path, viewBox = '0 0 24 24', className = 'mark') {
32
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
33
+ svg.setAttribute('viewBox', viewBox);
34
+ svg.setAttribute('class', className);
35
+ svg.setAttribute('aria-hidden', 'true');
36
+ svg.setAttribute('focusable', 'false');
37
+ const shape = document.createElementNS('http://www.w3.org/2000/svg', 'path');
38
+ shape.setAttribute('d', path);
39
+ svg.append(shape);
40
+ return svg;
41
+ }
@@ -1,20 +1,26 @@
1
1
  import type { Account, ConnectSession } from '../core/types.js';
2
2
  /** `flung-load`: the accounts were read, on first show and after every change. */
3
3
  export interface LoadDetail {
4
+ /** The connected accounts the panel shows, newest first. */
4
5
  accounts: Account[];
5
6
  }
6
7
  /** `flung-connect`: a connect or reconnect finished. `accounts` are the ones stored; Facebook stores one per page. */
7
8
  export interface ConnectDetail {
9
+ /** The connect session, or null when the person came back from a redirect. */
8
10
  session: ConnectSession | null;
11
+ /** The accounts stored. */
9
12
  accounts: Account[];
10
13
  }
11
14
  /** `flung-disconnect`: an account was disconnected. */
12
15
  export interface DisconnectDetail {
16
+ /** The account, now `disconnected`. */
13
17
  account: Account;
14
18
  }
15
19
  /** `flung-error`: a call or a connect failed. `code` is the API's stable code, such as `forbidden` or `access_denied`. */
16
20
  export interface ErrorDetail {
21
+ /** The API's stable code, or the platform's, such as `access_denied`. */
17
22
  code: string;
23
+ /** What went wrong, for a person. */
18
24
  message: string;
19
25
  }
20
26
  export interface FlungConnectEventMap {
@@ -2,14 +2,15 @@ import { ConnectClient, FlungConnectError } from '../core/client.js';
2
2
  import { connectInPopup, readRedirectResult, withoutRedirectResult } from '../core/flows.js';
3
3
  import { HANDLE_PROMPT, parsePlatformList, PLATFORM_NAMES, tokenPlatforms } from '../core/platforms.js';
4
4
  import { DEFAULT_LABELS, format } from './copy.js';
5
- import { button, displayName, html } from './dom.js';
5
+ import { button, displayName, html, mark } from './dom.js';
6
+ import { FLUNG_MARK, PLATFORM_MARKS } from './marks.js';
6
7
  import { STYLES } from './styles.js';
7
8
  /** How soon a reconnect deadline is worth showing. */
8
9
  const SOON_MS = 14 * 86_400_000;
9
10
  /** Builds the class on first use, so importing the package on a server, where there is no `HTMLElement`, is safe. */
10
11
  export function createFlungConnectClass() {
11
12
  return class FlungConnect extends HTMLElement {
12
- static observedAttributes = ['token', 'base-url', 'platforms'];
13
+ static observedAttributes = ['token', 'base-url', 'platforms', 'branding'];
13
14
  #root = this.attachShadow({ mode: 'open' });
14
15
  #source = null;
15
16
  #client = null;
@@ -50,7 +51,9 @@ export function createFlungConnectClass() {
50
51
  return;
51
52
  if (name === 'token')
52
53
  this.#source = next;
53
- if (name === 'platforms')
54
+ if (name === 'branding')
55
+ this.#render();
56
+ else if (name === 'platforms')
54
57
  void this.#load();
55
58
  else
56
59
  this.#reset();
@@ -184,7 +187,7 @@ export function createFlungConnectClass() {
184
187
  const active = this.#root.activeElement instanceof HTMLElement ? (this.#root.activeElement.dataset['key'] ?? null) : null;
185
188
  const list = html('ul', { class: 'list', part: 'list' }, ...this.#platforms.map((platform) => this.#platformRow(platform)));
186
189
  const message = html('p', { class: this.#message.error ? 'message error' : 'message', part: 'message', role: 'status', 'aria-live': 'polite' }, this.#message.text);
187
- const panel = html('section', { class: 'panel', part: 'panel', 'aria-label': this.#labels.panel }, list, message);
190
+ const panel = html('section', { class: 'panel', part: 'panel', 'aria-label': this.#labels.panel }, list, message, this.#footer());
188
191
  this.#root.replaceChildren(html('style', {}, STYLES), panel);
189
192
  const key = this.#focus ?? active;
190
193
  this.#focus = null;
@@ -196,15 +199,27 @@ export function createFlungConnectClass() {
196
199
  const state = this.#rows.get(platform) ?? { kind: 'idle' };
197
200
  const busy = state.kind === 'waiting';
198
201
  const accounts = this.#accounts.filter((account) => account.platform === platform);
199
- const connect = button(accounts.length === 0 ? this.#labels.connect : this.#labels.connectAnother, `connect-${platform}`, () => this.#askOrStart(platform), { primary: accounts.length === 0, disabled: busy, label: `${accounts.length === 0 ? this.#labels.connect : this.#labels.connectAnother} ${name}` });
200
- const row = html('div', { class: 'row' }, html('span', { class: 'badge', part: 'badge', 'aria-hidden': 'true' }, name.slice(0, 2)), html('span', { class: 'name', part: 'name' }, name), connect);
202
+ const first = accounts.length === 0;
203
+ const connect = button(first ? this.#labels.connect : this.#labels.connectAnother, `connect-${platform}`, () => this.#askOrStart(platform), { primary: first, disabled: busy, label: `${first ? this.#labels.connect : this.#labels.connectAnother} ${name}` });
204
+ const count = first ? this.#labels.notConnected : format(this.#labels.connectedCount, { count: String(accounts.length) });
205
+ const title = html('span', { class: 'title' }, html('span', { class: 'name', part: 'name' }, name), html('span', { class: first ? 'count' : 'count on' }, count));
206
+ const row = html('div', { class: 'row' }, html('span', { class: 'badge', part: 'badge', 'aria-hidden': 'true' }, mark(PLATFORM_MARKS[platform].path)), title, connect);
201
207
  const items = accounts.map((account) => this.#accountRow(account, busy));
202
208
  return html('li', { class: 'platform', part: 'platform', 'data-platform': platform }, row, items.length === 0 ? null : html('ul', { class: 'accounts' }, ...items), this.#rowExtra(platform, state));
203
209
  }
204
210
  #accountRow(account, busy) {
205
211
  const due = account.reconnectBy === null ? null : new Date(account.reconnectBy);
206
- const status = account.status === 'needs_reconnect' ? this.#labels.needsReconnect : due !== null && due.getTime() - Date.now() < SOON_MS ? format(this.#labels.reconnectBy, { date: due.toLocaleDateString() }) : null;
207
- return html('li', { class: 'account', part: 'account', 'data-account': account.id }, html('span', { class: 'who' }, displayName(account)), status === null ? null : html('span', { class: 'status', part: 'status' }, status), status === null ? null : button(this.#labels.reconnect, `reconnect-${account.id}`, () => this.#start(account.platform, account, undefined), { disabled: busy }), button(this.#labels.disconnect, `disconnect-${account.id}`, () => this.#confirm(account), { danger: true, disabled: busy, label: `${this.#labels.disconnect} ${displayName(account)}` }));
212
+ const status = account.status === 'needs_reconnect' ? this.#labels.needsReconnect : due !== null && due.getTime() - Date.now() < SOON_MS ? format(this.#labels.reconnectBy, { date: due.toLocaleDateString(undefined, { day: 'numeric', month: 'short' }) }) : null;
213
+ const reconnect = status === null ? null : button(this.#labels.reconnect, `reconnect-${account.id}`, () => this.#start(account.platform, account, undefined), { warning: true, disabled: busy });
214
+ const disconnect = button(this.#labels.disconnect, `disconnect-${account.id}`, () => this.#confirm(account), { quiet: true, disabled: busy, label: `${this.#labels.disconnect} ${displayName(account)}` });
215
+ return html('li', { class: 'account', part: 'account', 'data-account': account.id }, html('span', { class: 'who' }, html('span', { class: 'handle', title: displayName(account) }, displayName(account)), status === null ? null : html('span', { class: 'status', part: 'status' }, status)), html('span', { class: 'actions' }, reconnect, disconnect));
216
+ }
217
+ /** Flung's mark and name, linking to its docs, unless the page set `branding="none"`. */
218
+ #footer() {
219
+ if (this.getAttribute('branding') === 'none')
220
+ return null;
221
+ const link = html('a', { href: 'https://docs.flung.dev', target: '_blank', rel: 'noopener' }, mark(FLUNG_MARK.path, FLUNG_MARK.viewBox, 'flung'), 'Flung');
222
+ return html('p', { class: 'footer', part: 'footer' }, this.#labels.securedBy, link);
208
223
  }
209
224
  #rowExtra(platform, state) {
210
225
  const name = PLATFORM_NAMES[platform];
@@ -223,7 +238,7 @@ export function createFlungConnectClass() {
223
238
  if (state.kind === 'waiting') {
224
239
  const link = state.blockedUrl === null ? null : html('a', { href: state.blockedUrl, target: '_blank', rel: 'noopener' }, format(this.#labels.openSignIn, { platform: name }));
225
240
  const text = state.blockedUrl === null ? format(this.#labels.waiting, { platform: name }) : `${format(this.#labels.blocked, { platform: name })} `;
226
- return html('p', { class: 'note', part: 'note' }, text, link, ' ', button(this.#labels.cancel, `cancel-${platform}`, () => (state.abort.abort(), this.#idle(platform))));
241
+ return html('p', { class: 'note', part: 'note' }, html('span', { class: 'spinner', 'aria-hidden': 'true' }), html('span', { class: 'note-text' }, text, link), button(this.#labels.cancel, `cancel-${platform}`, () => (state.abort.abort(), this.#idle(platform))));
227
242
  }
228
243
  if (state.kind === 'confirm') {
229
244
  return html('div', { class: 'confirm', part: 'confirm' }, html('p', {}, format(this.#labels.confirmDisconnect, { account: displayName(state.account) })), button(this.#labels.disconnect, `confirm-${platform}`, () => void this.#disconnect(state.account), { danger: true }), button(this.#labels.keep, `keep-${platform}`, () => this.#idle(platform)));
@@ -0,0 +1,12 @@
1
+ import type { Platform } from '../core/types.js';
2
+ /** A platform's mark on a 24 by 24 grid, and its brand colour as six hex digits. */
3
+ export interface PlatformMark {
4
+ readonly path: string;
5
+ readonly hex: string;
6
+ }
7
+ export declare const PLATFORM_MARKS: Readonly<Record<Platform, PlatformMark>>;
8
+ /** Flung's mark, from https://assets.flung.dev/brand/logo/flung-mark-on-light.svg. */
9
+ export declare const FLUNG_MARK: {
10
+ readonly viewBox: '0 0 500 500';
11
+ readonly path: 'M 189.525 16.874 C 190.808 16.873 192.091 16.873 193.413 16.872 C 196.941 16.869 200.467 16.873 203.992 16.878 C 207.81 16.884 211.625 16.881 215.442 16.881 C 222.042 16.88 228.642 16.884 235.242 16.891 C 244.784 16.899 254.326 16.902 263.868 16.904 C 279.352 16.906 294.837 16.913 310.322 16.924 C 325.357 16.933 340.393 16.941 355.428 16.947 C 356.82 16.947 356.82 16.947 358.24 16.947 C 362.896 16.949 367.55 16.95 372.206 16.951 C 410.783 16.963 449.359 16.982 487.935 17.007 C 487.954 19.653 487.967 22.299 487.978 24.945 C 487.986 26.06 487.986 26.06 487.994 27.198 C 488.011 33.049 487.504 38.516 486.278 44.226 C 485.989 45.603 485.989 45.603 485.694 47.009 C 479.753 73.338 467.128 96.459 449.249 116.488 C 448.329 117.521 447.408 118.554 446.46 119.619 C 434.313 132.43 419.889 141.304 403.93 148.543 C 402.995 148.995 402.061 149.447 401.097 149.912 C 387.969 155.816 373.086 157.455 358.886 158.492 C 337.903 160.08 318.12 166.235 302.238 180.598 C 301.228 181.476 301.228 181.476 300.196 182.373 C 290.186 191.634 284.215 203.986 280.409 216.88 C 276.039 231.615 269.265 243.914 255.779 252.324 C 245.106 257.877 235.96 259.309 224.105 259.493 C 207.259 259.997 192.415 263.213 180.305 275.865 C 171.548 286.39 167.399 299.632 168.491 313.239 C 169.76 324.473 175.361 336.705 183.966 344.189 C 197.7 354.156 210.093 357.822 227.074 356.348 C 240.814 353.699 252.153 345.546 260.235 334.242 C 264.938 325.96 267.358 317.705 268.395 308.295 C 270.357 290.773 276.223 278.048 289.803 266.539 C 299.876 258.871 311.257 253.264 324.085 253.285 C 324.795 253.283 325.507 253.282 326.238 253.28 C 328.597 253.276 330.956 253.287 333.315 253.3 C 335.015 253.301 336.714 253.301 338.415 253.3 C 343.014 253.298 347.612 253.311 352.211 253.327 C 357.024 253.34 361.836 253.341 366.649 253.344 C 375.754 253.351 384.859 253.369 393.963 253.391 C 404.333 253.416 414.703 253.429 425.071 253.439 C 446.395 253.463 467.718 253.502 489.041 253.551 C 489.12 263.833 489.178 274.116 489.213 284.399 C 489.231 289.177 489.253 293.955 489.292 298.732 C 489.713 353.119 479.51 398.095 440.406 438.144 C 402.556 475.574 354.507 484.025 303.516 483.908 C 298.647 483.9 293.777 483.908 288.908 483.913 C 280.739 483.918 272.57 483.91 264.401 483.895 C 255.01 483.876 245.619 483.883 236.228 483.901 C 228.105 483.916 219.983 483.918 211.86 483.909 C 207.034 483.904 202.21 483.903 197.384 483.914 C 159.131 483.99 159.131 483.99 143.068 480.146 C 141.987 479.903 140.905 479.66 139.79 479.408 C 137.277 478.822 134.828 478.14 132.36 477.383 C 131.465 477.109 130.569 476.835 129.646 476.552 C 105.884 468.875 84.897 456.337 66.8 439.249 C 65.448 438.112 64.09 436.983 62.724 435.863 C 32.664 410.058 16.633 368.22 12.873 329.721 C 12.432 323.684 12.465 317.647 12.46 311.597 C 12.454 310.143 12.448 308.689 12.443 307.192 C 12.428 303.231 12.422 299.269 12.416 295.309 C 12.413 292.822 12.409 290.335 12.405 287.848 C 12.389 280.032 12.378 272.215 12.374 264.399 C 12.369 255.444 12.35 246.487 12.318 237.531 C 12.294 230.566 12.282 223.6 12.28 216.633 C 12.28 212.495 12.274 208.355 12.254 204.215 C 12.154 182.613 12.574 161.364 18.717 140.46 C 18.991 139.523 19.265 138.584 19.548 137.619 C 27.777 111.087 42.745 89.246 61.273 68.959 C 62.004 68.134 62.735 67.311 63.487 66.463 C 95.594 32.13 143.615 16.748 189.525 16.874 Z';
12
+ };
@@ -0,0 +1,28 @@
1
+ export const PLATFORM_MARKS = {
2
+ // Simple Icons x (CC0 1.0)
3
+ x: { hex: '000000', path: 'M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z' },
4
+ // Simple Icons instagram (CC0 1.0)
5
+ instagram: { hex: 'FF0069', path: 'M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077' },
6
+ // Simple Icons facebook (CC0 1.0)
7
+ facebook: { hex: '0866FF', path: 'M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z' },
8
+ // Simple Icons threads (CC0 1.0)
9
+ threads: { hex: '000000', path: 'M18.263 11.097c-.03-3.486-1.92-5.586-5.111-5.586-2.13 0-3.922.963-4.863 2.499l2.062 1.438c.535-.843 1.272-1.543 2.628-1.543 1.528 0 2.318.85 2.544 2.431a15 15 0 0 0-2.236-.173c-4.125 0-6.068 1.867-6.068 4.336s1.943 3.99 4.804 3.99c3.139 0 5.013-2.115 5.781-4.735.798.361 1.348 1.204 1.348 2.47 0 3.387-3.907 5.232-7.22 5.232-4.885 0-8.077-3.207-8.077-8.424 0-6.392 4.223-10.487 9.9-10.487 3.808 0 5.69 1.671 6.97 3.914l2.108-1.475C21.44 2.078 18.331 0 13.663 0 6.227 0 1.168 5.277 1.168 12.934c0 7 4.953 11.066 10.856 11.066 4.878 0 9.809-2.846 9.809-7.716 0-2.545-1.46-4.231-3.569-5.187m-6.33 4.855c-1.077 0-2.026-.512-2.026-1.453 0-1.483 1.822-1.934 3.606-1.934.678 0 1.34.045 1.927.173-.422 1.927-1.671 3.215-3.508 3.214Z' },
10
+ // Boxicons bxl-linkedin (MIT)
11
+ linkedin: { hex: '0A66C2', path: 'M2.7949999999999995 5.009a2.188 2.188 0 1 0 4.376 0a2.188 2.188 0 1 0 -4.376 0zM9.237 8.855v12.139h3.769v-6.003c0-1.584.298-3.118 2.262-3.118 1.937 0 1.961 1.811 1.961 3.218v5.904H21v-6.657c0-3.27-.704-5.783-4.526-5.783-1.835 0-3.065 1.007-3.568 1.96h-.051v-1.66H9.237zm-6.142 0H6.87v12.139H3.095z' },
12
+ // Simple Icons tiktok (CC0 1.0)
13
+ tiktok: { hex: '000000', path: 'M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z' },
14
+ // Simple Icons youtube (CC0 1.0)
15
+ youtube: { hex: 'FF0000', path: 'M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z' },
16
+ // Simple Icons pinterest (CC0 1.0)
17
+ pinterest: { hex: 'BD081C', path: 'M12.017 0C5.396 0 .029 5.367.029 11.987c0 5.079 3.158 9.417 7.618 11.162-.105-.949-.199-2.403.041-3.439.219-.937 1.406-5.957 1.406-5.957s-.359-.72-.359-1.781c0-1.663.967-2.911 2.168-2.911 1.024 0 1.518.769 1.518 1.688 0 1.029-.653 2.567-.992 3.992-.285 1.193.6 2.165 1.775 2.165 2.128 0 3.768-2.245 3.768-5.487 0-2.861-2.063-4.869-5.008-4.869-3.41 0-5.409 2.562-5.409 5.199 0 1.033.394 2.143.889 2.741.099.12.112.225.085.345-.09.375-.293 1.199-.334 1.363-.053.225-.172.271-.401.165-1.495-.69-2.433-2.878-2.433-4.646 0-3.776 2.748-7.252 7.92-7.252 4.158 0 7.392 2.967 7.392 6.923 0 4.135-2.607 7.462-6.233 7.462-1.214 0-2.354-.629-2.758-1.379l-.749 2.848c-.269 1.045-1.004 2.352-1.498 3.146 1.123.345 2.306.535 3.55.535 6.607 0 11.985-5.365 11.985-11.987C23.97 5.39 18.592.026 11.985.026L12.017 0z' },
18
+ // Simple Icons bluesky (CC0 1.0)
19
+ bluesky: { hex: '1185FE', path: 'M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037-.856 3.061-3.978 3.842-6.755 3.37 4.854.826 6.089 3.562 3.422 6.299-5.065 5.196-7.28-1.304-7.847-2.97-.104-.305-.152-.448-.153-.327 0-.121-.05.022-.153.327-.568 1.666-2.782 8.166-7.847 2.97-2.667-2.737-1.432-5.473 3.422-6.3-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026' },
20
+ // Simple Icons mastodon (CC0 1.0)
21
+ mastodon: { hex: '6364FF', path: 'M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z' },
22
+ // Simple Icons discord (CC0 1.0)
23
+ discord: { hex: '5865F2', path: 'M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z' },
24
+ // Simple Icons farcaster (CC0 1.0)
25
+ farcaster: { hex: '855DCD', path: 'M18.24.24H5.76C2.5789.24 0 2.8188 0 6v12c0 3.1811 2.5789 5.76 5.76 5.76h12.48c3.1812 0 5.76-2.5789 5.76-5.76V6C24 2.8188 21.4212.24 18.24.24m.8155 17.1662v.504c.2868-.0256.5458.1905.5439.479v.5688h-5.1437v-.5688c-.0019-.2885.2576-.5047.5443-.479v-.504c0-.22.1525-.402.358-.458l-.0095-4.3645c-.1589-1.7366-1.6402-3.0979-3.4435-3.0979-1.8038 0-3.2846 1.3613-3.4435 3.0979l-.0096 4.3578c.2276.0424.5318.2083.5395.4648v.504c.2863-.0256.5457.1905.5438.479v.5688H4.3915v-.5688c-.0019-.2885.2575-.5047.5438-.479v-.504c0-.2529.2011-.4548.4536-.4724v-7.895h-.4905L4.2898 7.008l2.6405-.0005V5.0419h9.9495v1.9656h2.8219l-.6091 2.0314h-.4901v7.8949c.2519.0177.453.2195.453.4724' },
26
+ };
27
+ /** Flung's mark, from https://assets.flung.dev/brand/logo/flung-mark-on-light.svg. */
28
+ export const FLUNG_MARK = { viewBox: '0 0 500 500', path: 'M 189.525 16.874 C 190.808 16.873 192.091 16.873 193.413 16.872 C 196.941 16.869 200.467 16.873 203.992 16.878 C 207.81 16.884 211.625 16.881 215.442 16.881 C 222.042 16.88 228.642 16.884 235.242 16.891 C 244.784 16.899 254.326 16.902 263.868 16.904 C 279.352 16.906 294.837 16.913 310.322 16.924 C 325.357 16.933 340.393 16.941 355.428 16.947 C 356.82 16.947 356.82 16.947 358.24 16.947 C 362.896 16.949 367.55 16.95 372.206 16.951 C 410.783 16.963 449.359 16.982 487.935 17.007 C 487.954 19.653 487.967 22.299 487.978 24.945 C 487.986 26.06 487.986 26.06 487.994 27.198 C 488.011 33.049 487.504 38.516 486.278 44.226 C 485.989 45.603 485.989 45.603 485.694 47.009 C 479.753 73.338 467.128 96.459 449.249 116.488 C 448.329 117.521 447.408 118.554 446.46 119.619 C 434.313 132.43 419.889 141.304 403.93 148.543 C 402.995 148.995 402.061 149.447 401.097 149.912 C 387.969 155.816 373.086 157.455 358.886 158.492 C 337.903 160.08 318.12 166.235 302.238 180.598 C 301.228 181.476 301.228 181.476 300.196 182.373 C 290.186 191.634 284.215 203.986 280.409 216.88 C 276.039 231.615 269.265 243.914 255.779 252.324 C 245.106 257.877 235.96 259.309 224.105 259.493 C 207.259 259.997 192.415 263.213 180.305 275.865 C 171.548 286.39 167.399 299.632 168.491 313.239 C 169.76 324.473 175.361 336.705 183.966 344.189 C 197.7 354.156 210.093 357.822 227.074 356.348 C 240.814 353.699 252.153 345.546 260.235 334.242 C 264.938 325.96 267.358 317.705 268.395 308.295 C 270.357 290.773 276.223 278.048 289.803 266.539 C 299.876 258.871 311.257 253.264 324.085 253.285 C 324.795 253.283 325.507 253.282 326.238 253.28 C 328.597 253.276 330.956 253.287 333.315 253.3 C 335.015 253.301 336.714 253.301 338.415 253.3 C 343.014 253.298 347.612 253.311 352.211 253.327 C 357.024 253.34 361.836 253.341 366.649 253.344 C 375.754 253.351 384.859 253.369 393.963 253.391 C 404.333 253.416 414.703 253.429 425.071 253.439 C 446.395 253.463 467.718 253.502 489.041 253.551 C 489.12 263.833 489.178 274.116 489.213 284.399 C 489.231 289.177 489.253 293.955 489.292 298.732 C 489.713 353.119 479.51 398.095 440.406 438.144 C 402.556 475.574 354.507 484.025 303.516 483.908 C 298.647 483.9 293.777 483.908 288.908 483.913 C 280.739 483.918 272.57 483.91 264.401 483.895 C 255.01 483.876 245.619 483.883 236.228 483.901 C 228.105 483.916 219.983 483.918 211.86 483.909 C 207.034 483.904 202.21 483.903 197.384 483.914 C 159.131 483.99 159.131 483.99 143.068 480.146 C 141.987 479.903 140.905 479.66 139.79 479.408 C 137.277 478.822 134.828 478.14 132.36 477.383 C 131.465 477.109 130.569 476.835 129.646 476.552 C 105.884 468.875 84.897 456.337 66.8 439.249 C 65.448 438.112 64.09 436.983 62.724 435.863 C 32.664 410.058 16.633 368.22 12.873 329.721 C 12.432 323.684 12.465 317.647 12.46 311.597 C 12.454 310.143 12.448 308.689 12.443 307.192 C 12.428 303.231 12.422 299.269 12.416 295.309 C 12.413 292.822 12.409 290.335 12.405 287.848 C 12.389 280.032 12.378 272.215 12.374 264.399 C 12.369 255.444 12.35 246.487 12.318 237.531 C 12.294 230.566 12.282 223.6 12.28 216.633 C 12.28 212.495 12.274 208.355 12.254 204.215 C 12.154 182.613 12.574 161.364 18.717 140.46 C 18.991 139.523 19.265 138.584 19.548 137.619 C 27.777 111.087 42.745 89.246 61.273 68.959 C 62.004 68.134 62.735 67.311 63.487 66.463 C 95.594 32.13 143.615 16.748 189.525 16.874 Z' };
@@ -3,4 +3,4 @@
3
3
  * page can set on the element; each part has a `part` name for `::part()`. Follows the page's light or dark scheme
4
4
  * unless `theme` says which.
5
5
  */
6
- export declare const STYLES = "\n:host {\n --flung-font: system-ui, -apple-system, \"Segoe UI\", sans-serif;\n --flung-text: #18181b;\n --flung-muted: #52525b;\n --flung-surface: #ffffff;\n --flung-raised: #f4f4f5;\n --flung-border: #d4d4d8;\n --flung-accent: #18181b;\n --flung-accent-text: #ffffff;\n --flung-danger: #b91c1c;\n --flung-warning: #a16207;\n --flung-radius: 8px;\n display: block;\n font: 15px/1.45 var(--flung-font);\n color: var(--flung-text);\n}\n:host([hidden]) { display: none; }\n@media (prefers-color-scheme: dark) {\n :host(:not([theme=\"light\"])) {\n --flung-text: #f4f4f5;\n --flung-muted: #a1a1aa;\n --flung-surface: #18181b;\n --flung-raised: #27272a;\n --flung-border: #3f3f46;\n --flung-accent: #f4f4f5;\n --flung-accent-text: #18181b;\n --flung-danger: #f87171;\n --flung-warning: #facc15;\n }\n}\n:host([theme=\"dark\"]) {\n --flung-text: #f4f4f5;\n --flung-muted: #a1a1aa;\n --flung-surface: #18181b;\n --flung-raised: #27272a;\n --flung-border: #3f3f46;\n --flung-accent: #f4f4f5;\n --flung-accent-text: #18181b;\n --flung-danger: #f87171;\n --flung-warning: #facc15;\n}\n* { box-sizing: border-box; }\n.panel { background: var(--flung-surface); border: 1px solid var(--flung-border); border-radius: var(--flung-radius); overflow: hidden; }\nul { list-style: none; margin: 0; padding: 0; }\n.platform { padding: 12px 14px; border-top: 1px solid var(--flung-border); }\n.platform:first-child { border-top: 0; }\n.row { display: flex; align-items: center; gap: 10px; min-height: 36px; }\n.badge { flex: none; width: 32px; height: 32px; border-radius: 50%; display: grid; place-items: center; background: var(--flung-raised); font-weight: 600; font-size: 13px; }\n.name { flex: 1; min-width: 0; font-weight: 600; }\n.accounts { margin: 6px 0 0 42px; }\n.account { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 4px 0; }\n.who { flex: 1; min-width: 8rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.status { font-size: 13px; color: var(--flung-warning); }\n.note { margin: 8px 0 0 42px; color: var(--flung-muted); font-size: 14px; }\n.note a { color: inherit; }\n.confirm, .form { margin: 8px 0 0 42px; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }\n.confirm p { flex-basis: 100%; margin: 0; }\n.form label { flex-basis: 100%; font-size: 14px; color: var(--flung-muted); }\n.form input { flex: 1; min-width: 10rem; font: inherit; color: inherit; background: var(--flung-surface); border: 1px solid var(--flung-border); border-radius: calc(var(--flung-radius) - 2px); padding: 6px 10px; }\nbutton { font: inherit; font-size: 14px; cursor: pointer; border-radius: calc(var(--flung-radius) - 2px); padding: 6px 12px; border: 1px solid var(--flung-border); background: transparent; color: inherit; }\nbutton.primary { background: var(--flung-accent); border-color: var(--flung-accent); color: var(--flung-accent-text); }\nbutton.danger { color: var(--flung-danger); border-color: currentColor; }\nbutton:disabled { opacity: .55; cursor: default; }\nbutton:focus-visible, input:focus-visible, a:focus-visible { outline: 2px solid var(--flung-accent); outline-offset: 2px; }\n.message { margin: 0; padding: 10px 14px; border-top: 1px solid var(--flung-border); color: var(--flung-muted); font-size: 14px; }\n.message:empty { display: none; }\n.message.error { color: var(--flung-danger); }\n@media (prefers-reduced-motion: no-preference) { button { transition: opacity .15s; } }\n";
6
+ export declare const STYLES: string;
@@ -1,3 +1,22 @@
1
+ import { PLATFORMS } from '../core/types.js';
2
+ import { PLATFORM_MARKS } from './marks.js';
3
+ /** Each platform's brand colour for its mark. A black mark takes the text colour, so it shows on a dark page too. */
4
+ const MARK_COLOURS = PLATFORMS.map((platform) => {
5
+ const hex = PLATFORM_MARKS[platform].hex;
6
+ return `.platform[data-platform="${platform}"] { --mark: ${hex === '000000' ? 'var(--flung-text)' : `#${hex}`}; }`;
7
+ }).join('\n');
8
+ const DARK = `
9
+ --flung-text: #f4f4f5;
10
+ --flung-muted: #a1a1aa;
11
+ --flung-surface: #18181b;
12
+ --flung-raised: #27272a;
13
+ --flung-border: #2e2e33;
14
+ --flung-accent: #f4f4f5;
15
+ --flung-accent-text: #18181b;
16
+ --flung-danger: #f87171;
17
+ --flung-warning: #fbbf24;
18
+ --flung-success: #4ade80;
19
+ --flung-mark-strength: 70%;`;
1
20
  /**
2
21
  * The panel's look, inside its shadow root. Every colour, the font and the corner radius are `--flung-*` properties a
3
22
  * page can set on the element; each part has a `part` name for `::part()`. Follows the page's light or dark scheme
@@ -7,69 +26,84 @@ export const STYLES = `
7
26
  :host {
8
27
  --flung-font: system-ui, -apple-system, "Segoe UI", sans-serif;
9
28
  --flung-text: #18181b;
10
- --flung-muted: #52525b;
29
+ --flung-muted: #71717a;
11
30
  --flung-surface: #ffffff;
12
31
  --flung-raised: #f4f4f5;
13
- --flung-border: #d4d4d8;
32
+ --flung-border: #e4e4e7;
14
33
  --flung-accent: #18181b;
15
34
  --flung-accent-text: #ffffff;
16
- --flung-danger: #b91c1c;
17
- --flung-warning: #a16207;
18
- --flung-radius: 8px;
35
+ --flung-danger: #dc2626;
36
+ --flung-warning: #b45309;
37
+ --flung-success: #15803d;
38
+ --flung-radius: 12px;
39
+ --flung-mark-strength: 100%;
19
40
  display: block;
20
41
  font: 15px/1.45 var(--flung-font);
21
42
  color: var(--flung-text);
43
+ -webkit-font-smoothing: antialiased;
22
44
  }
23
45
  :host([hidden]) { display: none; }
24
46
  @media (prefers-color-scheme: dark) {
25
- :host(:not([theme="light"])) {
26
- --flung-text: #f4f4f5;
27
- --flung-muted: #a1a1aa;
28
- --flung-surface: #18181b;
29
- --flung-raised: #27272a;
30
- --flung-border: #3f3f46;
31
- --flung-accent: #f4f4f5;
32
- --flung-accent-text: #18181b;
33
- --flung-danger: #f87171;
34
- --flung-warning: #facc15;
47
+ :host(:not([theme="light"])) {${DARK}
35
48
  }
36
49
  }
37
- :host([theme="dark"]) {
38
- --flung-text: #f4f4f5;
39
- --flung-muted: #a1a1aa;
40
- --flung-surface: #18181b;
41
- --flung-raised: #27272a;
42
- --flung-border: #3f3f46;
43
- --flung-accent: #f4f4f5;
44
- --flung-accent-text: #18181b;
45
- --flung-danger: #f87171;
46
- --flung-warning: #facc15;
50
+ :host([theme="dark"]) {${DARK}
47
51
  }
48
52
  * { box-sizing: border-box; }
49
53
  .panel { background: var(--flung-surface); border: 1px solid var(--flung-border); border-radius: var(--flung-radius); overflow: hidden; }
50
54
  ul { list-style: none; margin: 0; padding: 0; }
51
- .platform { padding: 12px 14px; border-top: 1px solid var(--flung-border); }
55
+ .platform { padding: 12px 16px; border-top: 1px solid var(--flung-border); }
52
56
  .platform:first-child { border-top: 0; }
53
- .row { display: flex; align-items: center; gap: 10px; min-height: 36px; }
54
- .badge { flex: none; width: 32px; height: 32px; border-radius: 50%; display: grid; place-items: center; background: var(--flung-raised); font-weight: 600; font-size: 13px; }
55
- .name { flex: 1; min-width: 0; font-weight: 600; }
56
- .accounts { margin: 6px 0 0 42px; }
57
- .account { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 4px 0; }
58
- .who { flex: 1; min-width: 8rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
59
- .status { font-size: 13px; color: var(--flung-warning); }
60
- .note { margin: 8px 0 0 42px; color: var(--flung-muted); font-size: 14px; }
61
- .note a { color: inherit; }
62
- .confirm, .form { margin: 8px 0 0 42px; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
63
- .confirm p { flex-basis: 100%; margin: 0; }
64
- .form label { flex-basis: 100%; font-size: 14px; color: var(--flung-muted); }
65
- .form input { flex: 1; min-width: 10rem; font: inherit; color: inherit; background: var(--flung-surface); border: 1px solid var(--flung-border); border-radius: calc(var(--flung-radius) - 2px); padding: 6px 10px; }
66
- button { font: inherit; font-size: 14px; cursor: pointer; border-radius: calc(var(--flung-radius) - 2px); padding: 6px 12px; border: 1px solid var(--flung-border); background: transparent; color: inherit; }
57
+ .row { display: flex; align-items: center; gap: 12px; min-height: 40px; }
58
+ .badge { flex: none; width: 40px; height: 40px; border-radius: calc(var(--flung-radius) - 2px); display: grid; place-items: center; color: color-mix(in srgb, var(--mark, var(--flung-text)) var(--flung-mark-strength), var(--flung-text)); background: color-mix(in srgb, var(--mark, var(--flung-text)) 10%, var(--flung-surface)); }
59
+ .mark { width: 20px; height: 20px; fill: currentColor; display: block; }
60
+ .title { flex: 1; min-width: 0; display: flex; flex-direction: column; }
61
+ .name { font-weight: 600; line-height: 1.25; }
62
+ .count { font-size: 13px; color: var(--flung-muted); line-height: 1.3; }
63
+ .count.on::before { content: ""; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--flung-success); margin-right: 6px; vertical-align: 2px; }
64
+ .accounts { margin: 10px 0 2px 52px; border-left: 1px solid var(--flung-border); }
65
+ .account { display: flex; align-items: center; gap: 8px; padding: 6px 0 6px 12px; min-height: 36px; }
66
+ .who { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; column-gap: 8px; row-gap: 2px; }
67
+ .handle { min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; }
68
+ .status { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 500; line-height: 1; padding: 4px 8px; border-radius: 999px; color: var(--flung-warning); background: color-mix(in srgb, var(--flung-warning) 12%, var(--flung-surface)); white-space: nowrap; }
69
+ .actions { flex: none; display: flex; gap: 4px; }
70
+ .note { margin: 10px 0 0 52px; color: var(--flung-muted); font-size: 14px; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
71
+ .note-text { flex: 1 1 12rem; min-width: 0; }
72
+ .note a { color: var(--flung-text); font-weight: 500; }
73
+ .spinner { width: 14px; height: 14px; border-radius: 50%; border: 2px solid var(--flung-border); border-top-color: var(--flung-text); flex: none; }
74
+ .confirm, .form { margin: 10px 0 0 52px; padding: 12px; border-radius: calc(var(--flung-radius) - 2px); background: var(--flung-raised); display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
75
+ .confirm p { flex-basis: 100%; margin: 0 0 2px; font-size: 14px; }
76
+ .form label { flex-basis: 100%; font-size: 13px; color: var(--flung-muted); }
77
+ .form input { flex: 1 1 12rem; min-width: 0; font: inherit; font-size: 14px; color: inherit; background: var(--flung-surface); border: 1px solid var(--flung-border); border-radius: calc(var(--flung-radius) - 4px); padding: 7px 10px; }
78
+ button { font: inherit; font-size: 14px; font-weight: 500; line-height: 1.2; cursor: pointer; border-radius: calc(var(--flung-radius) - 4px); padding: 8px 14px; border: 1px solid var(--flung-border); background: var(--flung-surface); color: inherit; white-space: nowrap; }
79
+ button:hover:not(:disabled) { background: var(--flung-raised); }
67
80
  button.primary { background: var(--flung-accent); border-color: var(--flung-accent); color: var(--flung-accent-text); }
68
- button.danger { color: var(--flung-danger); border-color: currentColor; }
69
- button:disabled { opacity: .55; cursor: default; }
81
+ button.primary:hover:not(:disabled) { background: color-mix(in srgb, var(--flung-accent) 85%, var(--flung-surface)); }
82
+ button.quiet { border-color: transparent; background: transparent; color: var(--flung-muted); padding: 6px 10px; font-size: 13px; }
83
+ button.quiet:hover:not(:disabled) { color: var(--flung-danger); background: color-mix(in srgb, var(--flung-danger) 8%, transparent); }
84
+ button.warning { padding: 6px 10px; font-size: 13px; color: var(--flung-warning); border-color: color-mix(in srgb, var(--flung-warning) 40%, var(--flung-border)); }
85
+ button.danger { background: var(--flung-danger); border-color: var(--flung-danger); color: #ffffff; }
86
+ button.danger:hover:not(:disabled) { background: color-mix(in srgb, var(--flung-danger) 85%, #000000); }
87
+ button:disabled { opacity: .5; cursor: default; }
70
88
  button:focus-visible, input:focus-visible, a:focus-visible { outline: 2px solid var(--flung-accent); outline-offset: 2px; }
71
- .message { margin: 0; padding: 10px 14px; border-top: 1px solid var(--flung-border); color: var(--flung-muted); font-size: 14px; }
89
+ .message { margin: 0; padding: 10px 16px; border-top: 1px solid var(--flung-border); color: var(--flung-muted); font-size: 14px; background: var(--flung-raised); }
72
90
  .message:empty { display: none; }
73
- .message.error { color: var(--flung-danger); }
74
- @media (prefers-reduced-motion: no-preference) { button { transition: opacity .15s; } }
91
+ .message.error { color: var(--flung-danger); background: color-mix(in srgb, var(--flung-danger) 8%, var(--flung-surface)); }
92
+ .footer { display: flex; align-items: center; justify-content: center; gap: 6px; padding: 8px 16px; border-top: 1px solid var(--flung-border); font-size: 12px; color: var(--flung-muted); }
93
+ .footer a { display: inline-flex; align-items: center; gap: 4px; color: inherit; text-decoration: none; font-weight: 600; }
94
+ .footer a:hover { color: var(--flung-text); }
95
+ .footer .flung { width: 14px; height: 14px; fill: currentColor; display: block; }
96
+ @media (max-width: 420px) {
97
+ .platform { padding: 12px; }
98
+ .accounts, .note, .confirm, .form { margin-left: 0; }
99
+ .accounts { border-left: 0; border-top: 1px solid var(--flung-border); margin-top: 10px; }
100
+ .account { padding-left: 0; }
101
+ .row > button { padding: 8px 12px; }
102
+ }
103
+ @media (prefers-reduced-motion: no-preference) {
104
+ button { transition: background-color .15s, color .15s, opacity .15s; }
105
+ .spinner { animation: flung-spin .8s linear infinite; }
106
+ }
107
+ @keyframes flung-spin { to { transform: rotate(360deg); } }
108
+ ${MARK_COLOURS}
75
109
  `;
@@ -18,13 +18,23 @@ export interface FlungConnectProps {
18
18
  platforms?: readonly Platform[] | undefined;
19
19
  /** `redirect` sends the whole page to the platform and back, for pages that cannot open windows. */
20
20
  mode?: 'popup' | 'redirect' | undefined;
21
+ /** `light` or `dark`. Follows the page's colour scheme when left out. */
21
22
  theme?: 'light' | 'dark' | undefined;
23
+ /** `none` hides the small "Secured by Flung" line under the list. */
24
+ branding?: 'none' | undefined;
25
+ /** Any of the panel's words, to put it in another language. */
22
26
  labels?: Partial<ConnectLabels> | undefined;
27
+ /** A class for the element itself. */
23
28
  className?: string | undefined;
29
+ /** Styles for the element, such as its `--flung-*` custom properties. */
24
30
  style?: CSSProperties | undefined;
31
+ /** The accounts were read: on first show and after every change. */
25
32
  onLoad?: ((detail: LoadDetail) => void) | undefined;
33
+ /** A connect or reconnect finished. */
26
34
  onConnect?: ((detail: ConnectDetail) => void) | undefined;
35
+ /** An account was disconnected. */
27
36
  onDisconnect?: ((detail: DisconnectDetail) => void) | undefined;
37
+ /** A call or a connect failed. */
28
38
  onError?: ((detail: ErrorDetail) => void) | undefined;
29
39
  }
30
40
  export declare const FlungConnect: import("react").ForwardRefExoticComponent<FlungConnectProps & import("react").RefAttributes<FlungConnectElement>>;
@@ -42,6 +42,7 @@ function FlungConnectInner(props, ref) {
42
42
  platforms: props.platforms?.join(','),
43
43
  mode: props.mode,
44
44
  theme: props.theme,
45
+ branding: props.branding,
45
46
  class: props.className,
46
47
  style: props.style,
47
48
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flung.dev/connect",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Let people connect and disconnect their social accounts from your page: a <flung-connect> web component and a React component for Flung, with no runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "type": "module",