@flung.dev/connect 0.1.2 → 0.3.0

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.
@@ -4,21 +4,41 @@
4
4
  */
5
5
  export declare const PLATFORMS: readonly ['x', 'instagram', 'facebook', 'threads', 'linkedin', 'tiktok', 'youtube', 'pinterest', 'bluesky', 'mastodon', 'discord', 'farcaster'];
6
6
  export type Platform = (typeof PLATFORMS)[number];
7
+ /**
8
+ * Another way to connect a platform, on an app of its own: LinkedIn's `pages` connects the company pages the person
9
+ * administers, one account per page, instead of their own profile.
10
+ */
11
+ export type ConnectVariant = 'pages';
12
+ export declare const CONNECT_VARIANTS: readonly ConnectVariant[];
7
13
  export interface PlatformInfo {
8
14
  platform: Platform;
9
15
  /** Whether this Flung has the platform's app credentials, so accounts can be connected. */
10
16
  connectable: boolean;
17
+ /** The connect variants set up here, such as LinkedIn's `pages`. Optional: an API older than the field leaves it out. */
18
+ variants?: string[] | undefined;
11
19
  }
12
20
  export type AccountStatus = 'active' | 'needs_reconnect' | 'disconnected';
21
+ /** A connected social account, as `GET /v1/accounts` returns it. */
13
22
  export interface Account {
23
+ /** Flung's id for the account. */
14
24
  id: string;
25
+ /** The platform it is on. */
15
26
  platform: Platform;
27
+ /** The platform's own id for the user. */
16
28
  platformUserId: string;
29
+ /** The handle on the platform, without the `@`, when it has one. */
17
30
  username: string | null;
31
+ /** The name the person shows on the platform. Optional: an API older than the field leaves it out. */
32
+ displayName?: string | null;
33
+ /** The person's profile picture on the platform, an `https` address. Optional: an API older than the field leaves it out. */
34
+ avatarUrl?: string | null;
35
+ /** The Flung profile it belongs to, or null for the whole workspace. */
18
36
  profileId: string | null;
37
+ /** `active`, `needs_reconnect` or `disconnected`. */
19
38
  status: AccountStatus;
20
39
  /** When the person must reconnect by, for platforms whose connections cannot be renewed forever. */
21
40
  reconnectBy: string | null;
41
+ /** When it was first connected. */
22
42
  createdAt: string;
23
43
  }
24
44
  export type ConnectSessionStatus = 'pending' | 'connected' | 'failed' | 'expired';
@@ -3,3 +3,4 @@
3
3
  * the API's own (`spec/openapi.json`): `PlatformInfo`, `Account` and `ConnectSession`, trimmed to what is used.
4
4
  */
5
5
  export const PLATFORMS = ['x', 'instagram', 'facebook', 'threads', 'linkedin', 'tiktok', 'youtube', 'pinterest', 'bluesky', 'mastodon', 'discord', 'farcaster'];
6
+ export const CONNECT_VARIANTS = ['pages'];
@@ -1,39 +1,3 @@
1
- /**
2
- * Every word the panel shows, in English. Pass `labels` to put it in another language; `{platform}`, `{account}`,
3
- * `{message}`, `{date}` and `{count}` are filled in where a label has them.
4
- */
5
- export interface ConnectLabels {
6
- /** The panel's name for a screen reader. */
7
- panel: string;
8
- connect: string;
9
- connectAnother: string;
10
- reconnect: string;
11
- disconnect: string;
12
- confirmDisconnect: string;
13
- keep: string;
14
- cancel: string;
15
- continue: string;
16
- handleBluesky: string;
17
- handleMastodon: string;
18
- waiting: string;
19
- blocked: string;
20
- openSignIn: string;
21
- connected: string;
22
- failed: string;
23
- expired: string;
24
- disconnected: string;
25
- needsReconnect: string;
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;
33
- loading: string;
34
- loadFailed: string;
35
- empty: string;
36
- noToken: string;
37
- }
38
- export declare const DEFAULT_LABELS: ConnectLabels;
39
- export declare function format(template: string, values: Readonly<Record<string, string>>): string;
1
+ /** The panel's words live in core, so the headless controller words its status line the same way as the element. */
2
+ export { DEFAULT_LABELS, formatLabel as format } from '../core/labels.js';
3
+ export type { ConnectLabels } from '../core/labels.js';
@@ -1,32 +1,2 @@
1
- export const DEFAULT_LABELS = {
2
- panel: 'Connected accounts',
3
- connect: 'Connect',
4
- connectAnother: 'Add another',
5
- reconnect: 'Reconnect',
6
- disconnect: 'Disconnect',
7
- confirmDisconnect: 'Disconnect {account}? Posts to it will stop.',
8
- keep: 'Keep it',
9
- cancel: 'Cancel',
10
- continue: 'Continue',
11
- handleBluesky: 'Your Bluesky handle, such as alice.bsky.social (optional)',
12
- handleMastodon: 'Your Mastodon server, such as mastodon.social',
13
- waiting: 'Finish signing in to {platform} in the window that opened.',
14
- blocked: 'Your browser did not open the {platform} window.',
15
- openSignIn: 'Open {platform} sign-in',
16
- connected: '{platform} is connected.',
17
- failed: '{platform} was not connected: {message}',
18
- expired: 'Signing in to {platform} took too long. Try again.',
19
- disconnected: '{account} is disconnected.',
20
- needsReconnect: 'Needs reconnecting',
21
- reconnectBy: 'Reconnect by {date}',
22
- connectedCount: '{count} connected',
23
- notConnected: 'Not connected',
24
- securedBy: 'Secured by',
25
- loading: 'Loading accounts…',
26
- loadFailed: 'Could not load accounts: {message}',
27
- empty: 'There is nothing to connect here.',
28
- noToken: 'This panel has no connect token yet.',
29
- };
30
- export function format(template, values) {
31
- return template.replace(/\{(\w+)\}/g, (whole, name) => values[name] ?? whole);
32
- }
1
+ /** The panel's words live in core, so the headless controller words its status line the same way as the element. */
2
+ export { DEFAULT_LABELS, formatLabel as format } from '../core/labels.js';
@@ -3,21 +3,37 @@
3
3
  * supplies (an account name, an error message) can become markup. Attributes go through `setAttribute`, text through
4
4
  * `textContent`.
5
5
  */
6
+ import type { AccountItem } from '../core/controller.js';
6
7
  import type { Account } from '../core/types.js';
7
8
  type Attributes = Readonly<Record<string, string | number | boolean | undefined>>;
8
9
  /** An HTML element of the element's own. `true` sets an empty attribute; `false` and `undefined` set none. */
9
10
  export declare function html<K extends keyof HTMLElementTagNameMap>(tag: K, attributes?: Attributes, ...children: (Node | string | null)[]): HTMLElementTagNameMap[K];
10
- /** How an account is named in the panel: its username, or its platform id when it has none. */
11
+ /** How an account is named in the panel's messages: its username, or its platform id when it has none. */
11
12
  export declare function displayName(account: Account): string;
12
- /** A button that calls `onClick`, with a `data-key` so focus can be put back on it after the panel is drawn again. */
13
- export declare function button(text: string, key: string, onClick: () => void, options?: {
14
- primary?: boolean;
15
- danger?: boolean;
16
- warning?: boolean;
17
- quiet?: boolean;
13
+ /** A button's look: `primary` for the first Connect, `secondary` for the rest, `quiet` for Disconnect, `danger` to confirm one. */
14
+ export type ButtonVariant = 'primary' | 'secondary' | 'quiet' | 'danger';
15
+ /**
16
+ * A button that calls `onClick`, with a `data-key` so focus can be put back on it after the panel is drawn again. Its
17
+ * `part` is `button`, `button-<variant>` and `button-<role>`, so a page can style every button, a kind, or one of them.
18
+ */
19
+ export declare function button(text: string, key: string, role: string, onClick: () => void, options?: {
20
+ variant?: ButtonVariant;
18
21
  disabled?: boolean;
19
22
  label?: string;
23
+ expanded?: boolean;
24
+ controls?: string;
20
25
  }): HTMLButtonElement;
21
26
  /** 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;
27
+ export declare function mark(path: string, options?: {
28
+ viewBox?: string;
29
+ className?: string;
30
+ part?: string;
31
+ size?: number;
32
+ }): SVGSVGElement;
33
+ /**
34
+ * An account's picture: an `<img>` that sends no referrer, loads lazily and has an empty `alt` (the name is beside it),
35
+ * or a circle with the account's initial when there is no picture or it does not load. `avatarUrl` is only ever an
36
+ * `https` address (the controller drops anything else).
37
+ */
38
+ export declare function avatar(item: AccountItem): HTMLElement;
23
39
  export {};
@@ -16,22 +16,40 @@ export function html(tag, attributes = {}, ...children) {
16
16
  element.append(child);
17
17
  return element;
18
18
  }
19
- /** How an account is named in the panel: its username, or its platform id when it has none. */
19
+ /** How an account is named in the panel's messages: its username, or its platform id when it has none. */
20
20
  export function displayName(account) {
21
21
  return account.username ?? account.platformUserId;
22
22
  }
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
- export function button(text, key, onClick, options = {}) {
25
- const className = [options.primary === true && 'primary', options.danger === true && 'danger', options.warning === true && 'warning', options.quiet === true && 'quiet'].filter(Boolean).join(' ') || undefined;
26
- const element = html('button', { type: 'button', class: className, 'data-key': key, disabled: options.disabled === true, 'aria-label': options.label }, text);
23
+ /**
24
+ * A button that calls `onClick`, with a `data-key` so focus can be put back on it after the panel is drawn again. Its
25
+ * `part` is `button`, `button-<variant>` and `button-<role>`, so a page can style every button, a kind, or one of them.
26
+ */
27
+ export function button(text, key, role, onClick, options = {}) {
28
+ const variant = options.variant ?? 'secondary';
29
+ const element = html('button', {
30
+ type: 'button',
31
+ class: variant,
32
+ part: `button button-${variant} button-${role}`,
33
+ 'data-key': key,
34
+ disabled: options.disabled === true,
35
+ 'aria-label': options.label,
36
+ // A button that opens a question inside the panel says whether it is open, and which question.
37
+ 'aria-expanded': options.expanded === undefined ? undefined : String(options.expanded),
38
+ 'aria-controls': options.controls,
39
+ }, text);
27
40
  element.addEventListener('click', onClick);
28
41
  return element;
29
42
  }
30
43
  /** 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') {
44
+ export function mark(path, options = {}) {
32
45
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
33
- svg.setAttribute('viewBox', viewBox);
34
- svg.setAttribute('class', className);
46
+ svg.setAttribute('viewBox', options.viewBox ?? '0 0 24 24');
47
+ svg.setAttribute('class', options.className ?? 'mark');
48
+ svg.setAttribute('part', options.part ?? 'mark');
49
+ // Presentation attributes: any stylesheet overrides them, and an unstyled panel still draws a sensible size.
50
+ svg.setAttribute('width', String(options.size ?? 20));
51
+ svg.setAttribute('height', String(options.size ?? 20));
52
+ svg.setAttribute('fill', 'currentColor');
35
53
  svg.setAttribute('aria-hidden', 'true');
36
54
  svg.setAttribute('focusable', 'false');
37
55
  const shape = document.createElementNS('http://www.w3.org/2000/svg', 'path');
@@ -39,3 +57,17 @@ export function mark(path, viewBox = '0 0 24 24', className = 'mark') {
39
57
  svg.append(shape);
40
58
  return svg;
41
59
  }
60
+ /**
61
+ * An account's picture: an `<img>` that sends no referrer, loads lazily and has an empty `alt` (the name is beside it),
62
+ * or a circle with the account's initial when there is no picture or it does not load. `avatarUrl` is only ever an
63
+ * `https` address (the controller drops anything else).
64
+ */
65
+ export function avatar(item) {
66
+ const initial = html('span', { class: 'avatar initial', part: 'avatar avatar-initial', 'aria-hidden': 'true' }, item.initial);
67
+ if (item.avatarUrl === null)
68
+ return initial;
69
+ const image = html('img', { class: 'avatar', part: 'avatar avatar-image', alt: '', referrerpolicy: 'no-referrer', loading: 'lazy', decoding: 'async', width: 32, height: 32 });
70
+ image.addEventListener('error', () => image.replaceWith(initial), { once: true });
71
+ image.src = item.avatarUrl;
72
+ return image;
73
+ }
@@ -1,31 +1,21 @@
1
- import type { Account, ConnectSession } from '../core/types.js';
2
- /** `flung-load`: the accounts were read, on first show and after every change. */
3
- export interface LoadDetail {
4
- /** The connected accounts the panel shows, newest first. */
5
- accounts: Account[];
6
- }
7
- /** `flung-connect`: a connect or reconnect finished. `accounts` are the ones stored; Facebook stores one per page. */
8
- export interface ConnectDetail {
9
- /** The connect session, or null when the person came back from a redirect. */
10
- session: ConnectSession | null;
11
- /** The accounts stored. */
12
- accounts: Account[];
13
- }
14
- /** `flung-disconnect`: an account was disconnected. */
15
- export interface DisconnectDetail {
16
- /** The account, now `disconnected`. */
17
- account: Account;
18
- }
19
- /** `flung-error`: a call or a connect failed. `code` is the API's stable code, such as `forbidden` or `access_denied`. */
20
- export interface ErrorDetail {
21
- /** The API's stable code, or the platform's, such as `access_denied`. */
22
- code: string;
23
- /** What went wrong, for a person. */
24
- message: string;
1
+ import type { ConnectState } from '../core/controller.js';
2
+ import type { ConnectDetail, DisconnectDetail, ErrorDetail, LoadDetail } from '../core/events.js';
3
+ export type { ConnectDetail, DisconnectDetail, ErrorDetail, LoadDetail } from '../core/events.js';
4
+ /** `flung-change`: the panel's state changed. Does not bubble; for a page that draws parts of the panel itself. */
5
+ export interface ChangeDetail {
6
+ /** The new snapshot. */
7
+ state: ConnectState;
25
8
  }
9
+ /** The element's events, for `addEventListener` types. */
26
10
  export interface FlungConnectEventMap {
11
+ /** The accounts were read. */
27
12
  'flung-load': CustomEvent<LoadDetail>;
13
+ /** A connect or reconnect finished. */
28
14
  'flung-connect': CustomEvent<ConnectDetail>;
15
+ /** An account was disconnected. */
29
16
  'flung-disconnect': CustomEvent<DisconnectDetail>;
17
+ /** A call or a connect failed. */
30
18
  'flung-error': CustomEvent<ErrorDetail>;
19
+ /** The state changed. */
20
+ 'flung-change': CustomEvent<ChangeDetail>;
31
21
  }
@@ -1,11 +1,31 @@
1
1
  import type { TokenSource } from '../core/client.js';
2
- import type { ConnectLabels } from './copy.js';
2
+ import type { ConnectController, ConnectOptions, ConnectState } from '../core/controller.js';
3
+ import type { ConnectLabels } from '../core/labels.js';
4
+ import type { Platform } from '../core/types.js';
5
+ /** How the platforms are laid out: `list` (the default) one under another, `grid` as cards side by side, `compact` tighter. */
6
+ export type FlungConnectLayout = 'list' | 'grid' | 'compact';
7
+ /**
8
+ * How a platform with a connect variant (LinkedIn: a personal profile or company pages) is offered: `choose` (the
9
+ * default) asks which after Connect, inside the panel; `tiles` shows each as its own platform, such as LinkedIn and
10
+ * LinkedIn Page.
11
+ */
12
+ export type FlungConnectVariants = 'choose' | 'tiles';
3
13
  /** `<flung-connect>` as a page sees it. */
4
14
  export interface FlungConnectElement extends HTMLElement {
5
15
  /** A connect token, or a function that fetches a fresh one from your server. The `token` attribute sets a string. */
6
16
  token: TokenSource | null;
17
+ /** Every word the panel shows. Setting it fills in the defaults for any left out. */
7
18
  get labels(): ConnectLabels;
8
19
  set labels(value: Partial<ConnectLabels> | undefined);
20
+ /** The panel's current state, the same snapshot `flung-change` carries. */
21
+ readonly state: ConnectState;
22
+ /** The headless controller behind the panel, for a page that draws parts of it itself; null until the element is on a page. A new token makes a new one. */
23
+ readonly controller: ConnectController | null;
24
+ /**
25
+ * Starts a connect, as the platform's Connect button does. Call it straight from a click: a browser opens a window only
26
+ * during one. `{ variant: 'pages' }` connects LinkedIn company pages without asking, `{ variant: null }` the profile.
27
+ */
28
+ connect(platform: Platform, options?: ConnectOptions): void;
9
29
  /** Reads the accounts again, as after a connect. */
10
30
  refresh(): Promise<void>;
11
31
  }