@flung.dev/connect 0.1.2 → 0.4.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.
- package/README.md +224 -12
- package/dist/core/client.d.ts +11 -3
- package/dist/core/client.js +34 -12
- package/dist/core/controller.d.ts +232 -0
- package/dist/core/controller.js +447 -0
- package/dist/core/events.d.ts +25 -0
- package/dist/core/events.js +1 -0
- package/dist/core/index.d.ts +12 -4
- package/dist/core/index.js +7 -3
- package/dist/core/labels.d.ts +96 -0
- package/dist/core/labels.js +50 -0
- package/dist/core/platforms.d.ts +7 -1
- package/dist/core/platforms.js +11 -1
- package/dist/core/qr.d.ts +13 -0
- package/dist/core/qr.js +319 -0
- package/dist/core/types.d.ts +28 -0
- package/dist/core/types.js +1 -0
- package/dist/element/copy.d.ts +3 -39
- package/dist/element/copy.js +2 -32
- package/dist/element/dom.d.ts +29 -8
- package/dist/element/dom.js +77 -8
- package/dist/element/events.d.ts +14 -24
- package/dist/element/flung-connect.d.ts +21 -1
- package/dist/element/flung-connect.js +248 -198
- package/dist/element/marks.d.ts +7 -2
- package/dist/element/marks.js +8 -1
- package/dist/element/styles.d.ts +5 -3
- package/dist/element/styles.js +168 -42
- package/dist/element/tooltip.d.ts +26 -0
- package/dist/element/tooltip.js +135 -0
- package/dist/index.d.ts +2 -4
- package/dist/index.js +0 -1
- package/dist/react/index.d.ts +43 -3
- package/dist/react/index.js +39 -2
- package/dist/react/use-flung-connect.d.ts +13 -0
- package/dist/react/use-flung-connect.js +65 -0
- package/package.json +1 -1
package/dist/core/platforms.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Platform } from './types.js';
|
|
1
|
+
import type { Account, ConnectVariant, Platform } from './types.js';
|
|
2
2
|
export declare const PLATFORM_NAMES: Readonly<Record<Platform, string>>;
|
|
3
3
|
/** What a connect asks the person for first: Mastodon needs their instance; Bluesky takes a handle when they have one. */
|
|
4
4
|
export declare const HANDLE_PROMPT: Readonly<Partial<Record<Platform, {
|
|
@@ -12,3 +12,9 @@ export declare function parsePlatformList(value: string | null | undefined): Pla
|
|
|
12
12
|
* API checks the signature and refuses the rest. Null when the token names none or cannot be read.
|
|
13
13
|
*/
|
|
14
14
|
export declare function tokenPlatforms(token: string): Platform[] | null;
|
|
15
|
+
export declare function isConnectVariant(value: unknown): value is ConnectVariant;
|
|
16
|
+
/**
|
|
17
|
+
* Whether an account is a company page rather than a person: a LinkedIn account that posts as an organisation
|
|
18
|
+
* (`urn:li:organization:<id>`), as the `pages` variant connects them.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isPageAccount(account: Pick<Account, 'platform' | 'platformUserId'>): boolean;
|
package/dist/core/platforms.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PLATFORMS } from './types.js';
|
|
1
|
+
import { CONNECT_VARIANTS, PLATFORMS } from './types.js';
|
|
2
2
|
export const PLATFORM_NAMES = {
|
|
3
3
|
x: 'X',
|
|
4
4
|
instagram: 'Instagram',
|
|
@@ -47,3 +47,13 @@ export function tokenPlatforms(token) {
|
|
|
47
47
|
return null;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
export function isConnectVariant(value) {
|
|
51
|
+
return typeof value === 'string' && CONNECT_VARIANTS.includes(value);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Whether an account is a company page rather than a person: a LinkedIn account that posts as an organisation
|
|
55
|
+
* (`urn:li:organization:<id>`), as the `pages` variant connects them.
|
|
56
|
+
*/
|
|
57
|
+
export function isPageAccount(account) {
|
|
58
|
+
return account.platform === 'linkedin' && account.platformUserId.startsWith('urn:li:organization:');
|
|
59
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A QR code of a short text, such as the link that opens Warpcast to approve Flung: byte mode, error correction level M,
|
|
3
|
+
* the smallest version that holds it, and the mask with the lowest penalty. Written here so the package keeps no runtime
|
|
4
|
+
* dependency. It follows Project Nayuki's QR Code generator (MIT, https://www.nayuki.io/page/qr-code-generator-library)
|
|
5
|
+
* step for step, so its output is that library's, module for module.
|
|
6
|
+
*/
|
|
7
|
+
/** The code as rows of modules, `true` for dark, with no quiet zone: draw one of at least four light modules around it. */
|
|
8
|
+
export interface QrMatrix {
|
|
9
|
+
readonly size: number;
|
|
10
|
+
readonly modules: readonly (readonly boolean[])[];
|
|
11
|
+
}
|
|
12
|
+
/** Encodes `text` as UTF-8. Throws a `RangeError` when it is too long for a QR code (about 2,300 bytes at level M). */
|
|
13
|
+
export declare function encodeQr(text: string): QrMatrix;
|
package/dist/core/qr.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A QR code of a short text, such as the link that opens Warpcast to approve Flung: byte mode, error correction level M,
|
|
3
|
+
* the smallest version that holds it, and the mask with the lowest penalty. Written here so the package keeps no runtime
|
|
4
|
+
* dependency. It follows Project Nayuki's QR Code generator (MIT, https://www.nayuki.io/page/qr-code-generator-library)
|
|
5
|
+
* step for step, so its output is that library's, module for module.
|
|
6
|
+
*/
|
|
7
|
+
/** Error correction codewords per block at level M, by version (index 0 unused). */
|
|
8
|
+
const ECC_PER_BLOCK = [-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28];
|
|
9
|
+
/** Error correction blocks at level M, by version (index 0 unused). */
|
|
10
|
+
const BLOCKS = [-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49];
|
|
11
|
+
/** Level M's two format bits. */
|
|
12
|
+
const FORMAT_BITS_M = 0;
|
|
13
|
+
const PENALTY_N1 = 3;
|
|
14
|
+
const PENALTY_N2 = 3;
|
|
15
|
+
const PENALTY_N3 = 40;
|
|
16
|
+
const PENALTY_N4 = 10;
|
|
17
|
+
/** Encodes `text` as UTF-8. Throws a `RangeError` when it is too long for a QR code (about 2,300 bytes at level M). */
|
|
18
|
+
export function encodeQr(text) {
|
|
19
|
+
const bytes = new TextEncoder().encode(text);
|
|
20
|
+
let version = 1;
|
|
21
|
+
for (;; version += 1) {
|
|
22
|
+
if (version > 40)
|
|
23
|
+
throw new RangeError('The text is too long for a QR code.');
|
|
24
|
+
if (4 + countBits(version) + bytes.length * 8 <= dataCodewords(version) * 8)
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
const bits = [];
|
|
28
|
+
const append = (value, length) => {
|
|
29
|
+
for (let i = length - 1; i >= 0; i -= 1)
|
|
30
|
+
bits.push((value >>> i) & 1);
|
|
31
|
+
};
|
|
32
|
+
append(0x4, 4);
|
|
33
|
+
append(bytes.length, countBits(version));
|
|
34
|
+
for (const byte of bytes)
|
|
35
|
+
append(byte, 8);
|
|
36
|
+
const capacity = dataCodewords(version) * 8;
|
|
37
|
+
append(0, Math.min(4, capacity - bits.length));
|
|
38
|
+
append(0, (8 - (bits.length % 8)) % 8);
|
|
39
|
+
for (let pad = 0xec; bits.length < capacity; pad ^= 0xec ^ 0x11)
|
|
40
|
+
append(pad, 8);
|
|
41
|
+
const data = new Array(bits.length / 8).fill(0);
|
|
42
|
+
bits.forEach((bit, i) => {
|
|
43
|
+
data[i >>> 3] = data[i >>> 3] | (bit << (7 - (i & 7)));
|
|
44
|
+
});
|
|
45
|
+
return new Grid(version).draw(interleave(version, data));
|
|
46
|
+
}
|
|
47
|
+
const countBits = (version) => (version <= 9 ? 8 : 16);
|
|
48
|
+
function rawDataModules(version) {
|
|
49
|
+
let result = (16 * version + 128) * version + 64;
|
|
50
|
+
if (version >= 2) {
|
|
51
|
+
const align = Math.floor(version / 7) + 2;
|
|
52
|
+
result -= (25 * align - 10) * align - 55;
|
|
53
|
+
if (version >= 7)
|
|
54
|
+
result -= 36;
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
const dataCodewords = (version) => Math.floor(rawDataModules(version) / 8) - ECC_PER_BLOCK[version] * BLOCKS[version];
|
|
59
|
+
/** Splits the data into blocks, adds each block's Reed-Solomon codewords, and interleaves them. */
|
|
60
|
+
function interleave(version, data) {
|
|
61
|
+
const blocks = BLOCKS[version];
|
|
62
|
+
const eccLength = ECC_PER_BLOCK[version];
|
|
63
|
+
const raw = Math.floor(rawDataModules(version) / 8);
|
|
64
|
+
const shortBlocks = blocks - (raw % blocks);
|
|
65
|
+
const shortLength = Math.floor(raw / blocks);
|
|
66
|
+
const divisor = rsDivisor(eccLength);
|
|
67
|
+
const split = [];
|
|
68
|
+
for (let i = 0, k = 0; i < blocks; i += 1) {
|
|
69
|
+
const block = data.slice(k, k + shortLength - eccLength + (i < shortBlocks ? 0 : 1));
|
|
70
|
+
k += block.length;
|
|
71
|
+
const ecc = rsRemainder(block, divisor);
|
|
72
|
+
if (i < shortBlocks)
|
|
73
|
+
block.push(0);
|
|
74
|
+
split.push(block.concat(ecc));
|
|
75
|
+
}
|
|
76
|
+
const result = [];
|
|
77
|
+
for (let i = 0; i < split[0].length; i += 1) {
|
|
78
|
+
split.forEach((block, j) => {
|
|
79
|
+
if (i !== shortLength - eccLength || j >= shortBlocks)
|
|
80
|
+
result.push(block[i]);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
function rsMultiply(x, y) {
|
|
86
|
+
let z = 0;
|
|
87
|
+
for (let i = 7; i >= 0; i -= 1) {
|
|
88
|
+
z = (z << 1) ^ ((z >>> 7) * 0x11d);
|
|
89
|
+
z ^= ((y >>> i) & 1) * x;
|
|
90
|
+
}
|
|
91
|
+
return z;
|
|
92
|
+
}
|
|
93
|
+
function rsDivisor(degree) {
|
|
94
|
+
const result = new Array(degree - 1).fill(0).concat([1]);
|
|
95
|
+
let root = 1;
|
|
96
|
+
for (let i = 0; i < degree; i += 1) {
|
|
97
|
+
for (let j = 0; j < result.length; j += 1) {
|
|
98
|
+
result[j] = rsMultiply(result[j], root);
|
|
99
|
+
if (j + 1 < result.length)
|
|
100
|
+
result[j] = result[j] ^ result[j + 1];
|
|
101
|
+
}
|
|
102
|
+
root = rsMultiply(root, 0x02);
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
function rsRemainder(data, divisor) {
|
|
107
|
+
const result = divisor.map(() => 0);
|
|
108
|
+
for (const byte of data) {
|
|
109
|
+
const factor = byte ^ result.shift();
|
|
110
|
+
result.push(0);
|
|
111
|
+
divisor.forEach((coefficient, i) => {
|
|
112
|
+
result[i] = result[i] ^ rsMultiply(coefficient, factor);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
const bit = (value, i) => ((value >>> i) & 1) !== 0;
|
|
118
|
+
class Grid {
|
|
119
|
+
#size;
|
|
120
|
+
#version;
|
|
121
|
+
#modules;
|
|
122
|
+
#fixed;
|
|
123
|
+
constructor(version) {
|
|
124
|
+
this.#version = version;
|
|
125
|
+
this.#size = version * 4 + 17;
|
|
126
|
+
this.#modules = Array.from({ length: this.#size }, () => new Array(this.#size).fill(false));
|
|
127
|
+
this.#fixed = Array.from({ length: this.#size }, () => new Array(this.#size).fill(false));
|
|
128
|
+
}
|
|
129
|
+
draw(codewords) {
|
|
130
|
+
this.#functionPatterns();
|
|
131
|
+
this.#codewords(codewords);
|
|
132
|
+
let best = 0;
|
|
133
|
+
let lowest = Infinity;
|
|
134
|
+
for (let mask = 0; mask < 8; mask += 1) {
|
|
135
|
+
this.#mask(mask);
|
|
136
|
+
this.#format(mask);
|
|
137
|
+
const penalty = this.#penalty();
|
|
138
|
+
if (penalty < lowest) {
|
|
139
|
+
best = mask;
|
|
140
|
+
lowest = penalty;
|
|
141
|
+
}
|
|
142
|
+
this.#mask(mask);
|
|
143
|
+
}
|
|
144
|
+
this.#mask(best);
|
|
145
|
+
this.#format(best);
|
|
146
|
+
return { size: this.#size, modules: this.#modules.map((row) => [...row]) };
|
|
147
|
+
}
|
|
148
|
+
#set(x, y, dark) {
|
|
149
|
+
;
|
|
150
|
+
this.#modules[y][x] = dark;
|
|
151
|
+
this.#fixed[y][x] = true;
|
|
152
|
+
}
|
|
153
|
+
#functionPatterns() {
|
|
154
|
+
const size = this.#size;
|
|
155
|
+
for (let i = 0; i < size; i += 1) {
|
|
156
|
+
this.#set(6, i, i % 2 === 0);
|
|
157
|
+
this.#set(i, 6, i % 2 === 0);
|
|
158
|
+
}
|
|
159
|
+
this.#finder(3, 3);
|
|
160
|
+
this.#finder(size - 4, 3);
|
|
161
|
+
this.#finder(3, size - 4);
|
|
162
|
+
const positions = this.#alignmentPositions();
|
|
163
|
+
const last = positions.length - 1;
|
|
164
|
+
positions.forEach((x, i) => {
|
|
165
|
+
positions.forEach((y, j) => {
|
|
166
|
+
if (!((i === 0 && j === 0) || (i === 0 && j === last) || (i === last && j === 0)))
|
|
167
|
+
this.#alignment(x, y);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
this.#format(0);
|
|
171
|
+
this.#versionInfo();
|
|
172
|
+
}
|
|
173
|
+
#finder(x, y) {
|
|
174
|
+
for (let dy = -4; dy <= 4; dy += 1) {
|
|
175
|
+
for (let dx = -4; dx <= 4; dx += 1) {
|
|
176
|
+
const distance = Math.max(Math.abs(dx), Math.abs(dy));
|
|
177
|
+
const xx = x + dx;
|
|
178
|
+
const yy = y + dy;
|
|
179
|
+
if (xx >= 0 && xx < this.#size && yy >= 0 && yy < this.#size)
|
|
180
|
+
this.#set(xx, yy, distance !== 2 && distance !== 4);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
#alignment(x, y) {
|
|
185
|
+
for (let dy = -2; dy <= 2; dy += 1)
|
|
186
|
+
for (let dx = -2; dx <= 2; dx += 1)
|
|
187
|
+
this.#set(x + dx, y + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
|
|
188
|
+
}
|
|
189
|
+
#alignmentPositions() {
|
|
190
|
+
if (this.#version === 1)
|
|
191
|
+
return [];
|
|
192
|
+
const count = Math.floor(this.#version / 7) + 2;
|
|
193
|
+
const step = Math.floor((this.#version * 8 + count * 3 + 5) / (count * 4 - 4)) * 2;
|
|
194
|
+
const result = [6];
|
|
195
|
+
for (let position = this.#size - 7; result.length < count; position -= step)
|
|
196
|
+
result.splice(1, 0, position);
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
#format(mask) {
|
|
200
|
+
const data = (FORMAT_BITS_M << 3) | mask;
|
|
201
|
+
let remainder = data;
|
|
202
|
+
for (let i = 0; i < 10; i += 1)
|
|
203
|
+
remainder = (remainder << 1) ^ ((remainder >>> 9) * 0x537);
|
|
204
|
+
const bits = ((data << 10) | remainder) ^ 0x5412;
|
|
205
|
+
const size = this.#size;
|
|
206
|
+
for (let i = 0; i <= 5; i += 1)
|
|
207
|
+
this.#set(8, i, bit(bits, i));
|
|
208
|
+
this.#set(8, 7, bit(bits, 6));
|
|
209
|
+
this.#set(8, 8, bit(bits, 7));
|
|
210
|
+
this.#set(7, 8, bit(bits, 8));
|
|
211
|
+
for (let i = 9; i < 15; i += 1)
|
|
212
|
+
this.#set(14 - i, 8, bit(bits, i));
|
|
213
|
+
for (let i = 0; i < 8; i += 1)
|
|
214
|
+
this.#set(size - 1 - i, 8, bit(bits, i));
|
|
215
|
+
for (let i = 8; i < 15; i += 1)
|
|
216
|
+
this.#set(8, size - 15 + i, bit(bits, i));
|
|
217
|
+
this.#set(8, size - 8, true);
|
|
218
|
+
}
|
|
219
|
+
#versionInfo() {
|
|
220
|
+
if (this.#version < 7)
|
|
221
|
+
return;
|
|
222
|
+
let remainder = this.#version;
|
|
223
|
+
for (let i = 0; i < 12; i += 1)
|
|
224
|
+
remainder = (remainder << 1) ^ ((remainder >>> 11) * 0x1f25);
|
|
225
|
+
const bits = (this.#version << 12) | remainder;
|
|
226
|
+
for (let i = 0; i < 18; i += 1) {
|
|
227
|
+
const a = this.#size - 11 + (i % 3);
|
|
228
|
+
const b = Math.floor(i / 3);
|
|
229
|
+
this.#set(a, b, bit(bits, i));
|
|
230
|
+
this.#set(b, a, bit(bits, i));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
#codewords(data) {
|
|
234
|
+
const size = this.#size;
|
|
235
|
+
let i = 0;
|
|
236
|
+
for (let right = size - 1; right >= 1; right -= 2) {
|
|
237
|
+
if (right === 6)
|
|
238
|
+
right = 5;
|
|
239
|
+
for (let vertical = 0; vertical < size; vertical += 1) {
|
|
240
|
+
for (let j = 0; j < 2; j += 1) {
|
|
241
|
+
const x = right - j;
|
|
242
|
+
const upward = ((right + 1) & 2) === 0;
|
|
243
|
+
const y = upward ? size - 1 - vertical : vertical;
|
|
244
|
+
if (!this.#fixed[y][x] && i < data.length * 8) {
|
|
245
|
+
;
|
|
246
|
+
this.#modules[y][x] = bit(data[i >>> 3], 7 - (i & 7));
|
|
247
|
+
i += 1;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** Flips the data modules under `mask`; applying it twice undoes it. */
|
|
254
|
+
#mask(mask) {
|
|
255
|
+
for (let y = 0; y < this.#size; y += 1) {
|
|
256
|
+
for (let x = 0; x < this.#size; x += 1) {
|
|
257
|
+
const flip = [(x + y) % 2 === 0, y % 2 === 0, x % 3 === 0, (x + y) % 3 === 0, (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0, ((x * y) % 2) + ((x * y) % 3) === 0, (((x * y) % 2) + ((x * y) % 3)) % 2 === 0, (((x + y) % 2) + ((x * y) % 3)) % 2 === 0][mask];
|
|
258
|
+
const row = this.#modules[y];
|
|
259
|
+
if (flip === true && !this.#fixed[y][x])
|
|
260
|
+
row[x] = !row[x];
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
#penalty() {
|
|
265
|
+
const size = this.#size;
|
|
266
|
+
const at = (x, y) => this.#modules[y][x];
|
|
267
|
+
let result = 0;
|
|
268
|
+
for (const across of [true, false]) {
|
|
269
|
+
for (let a = 0; a < size; a += 1) {
|
|
270
|
+
let color = false;
|
|
271
|
+
let run = 0;
|
|
272
|
+
const history = [0, 0, 0, 0, 0, 0, 0];
|
|
273
|
+
for (let b = 0; b < size; b += 1) {
|
|
274
|
+
const dark = across ? at(b, a) : at(a, b);
|
|
275
|
+
if (dark === color) {
|
|
276
|
+
run += 1;
|
|
277
|
+
if (run === 5)
|
|
278
|
+
result += PENALTY_N1;
|
|
279
|
+
else if (run > 5)
|
|
280
|
+
result += 1;
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
this.#history(run, history);
|
|
284
|
+
if (!color)
|
|
285
|
+
result += this.#finderLike(history) * PENALTY_N3;
|
|
286
|
+
color = dark;
|
|
287
|
+
run = 1;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (color) {
|
|
291
|
+
this.#history(run, history);
|
|
292
|
+
run = 0;
|
|
293
|
+
}
|
|
294
|
+
this.#history(run + size, history);
|
|
295
|
+
result += this.#finderLike(history) * PENALTY_N3;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
for (let y = 0; y < size - 1; y += 1) {
|
|
299
|
+
for (let x = 0; x < size - 1; x += 1) {
|
|
300
|
+
const color = at(x, y);
|
|
301
|
+
if (color === at(x + 1, y) && color === at(x, y + 1) && color === at(x + 1, y + 1))
|
|
302
|
+
result += PENALTY_N2;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const dark = this.#modules.reduce((sum, row) => sum + row.filter(Boolean).length, 0);
|
|
306
|
+
const total = size * size;
|
|
307
|
+
result += (Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1) * PENALTY_N4;
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
#history(run, history) {
|
|
311
|
+
history.pop();
|
|
312
|
+
history.unshift(history[0] === 0 ? run + this.#size : run);
|
|
313
|
+
}
|
|
314
|
+
#finderLike(history) {
|
|
315
|
+
const [h0, n, h2, h3, h4, h5, h6] = history;
|
|
316
|
+
const core = n > 0 && h2 === n && h3 === n * 3 && h4 === n && h5 === n;
|
|
317
|
+
return (core && h0 >= n * 4 && h6 >= n ? 1 : 0) + (core && h6 >= n * 4 && h0 >= n ? 1 : 0);
|
|
318
|
+
}
|
|
319
|
+
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -4,21 +4,49 @@
|
|
|
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;
|
|
19
|
+
/**
|
|
20
|
+
* How a connect ends: `consent`, a sign-in page that comes back; `approval` (Farcaster), a link that opens the
|
|
21
|
+
* platform's phone app to approve, after which the session finishes when read. Optional: an API older than the field
|
|
22
|
+
* leaves it out, which reads as `consent`.
|
|
23
|
+
*/
|
|
24
|
+
connectFlow?: ConnectFlow | undefined;
|
|
11
25
|
}
|
|
26
|
+
/** How a platform's connect ends: a sign-in page that sends the person back, or an approval in the platform's phone app. */
|
|
27
|
+
export type ConnectFlow = 'consent' | 'approval';
|
|
12
28
|
export type AccountStatus = 'active' | 'needs_reconnect' | 'disconnected';
|
|
29
|
+
/** A connected social account, as `GET /v1/accounts` returns it. */
|
|
13
30
|
export interface Account {
|
|
31
|
+
/** Flung's id for the account. */
|
|
14
32
|
id: string;
|
|
33
|
+
/** The platform it is on. */
|
|
15
34
|
platform: Platform;
|
|
35
|
+
/** The platform's own id for the user. */
|
|
16
36
|
platformUserId: string;
|
|
37
|
+
/** The handle on the platform, without the `@`, when it has one. */
|
|
17
38
|
username: string | null;
|
|
39
|
+
/** The name the person shows on the platform. Optional: an API older than the field leaves it out. */
|
|
40
|
+
displayName?: string | null;
|
|
41
|
+
/** The person's profile picture on the platform, an `https` address. Optional: an API older than the field leaves it out. */
|
|
42
|
+
avatarUrl?: string | null;
|
|
43
|
+
/** The Flung profile it belongs to, or null for the whole workspace. */
|
|
18
44
|
profileId: string | null;
|
|
45
|
+
/** `active`, `needs_reconnect` or `disconnected`. */
|
|
19
46
|
status: AccountStatus;
|
|
20
47
|
/** When the person must reconnect by, for platforms whose connections cannot be renewed forever. */
|
|
21
48
|
reconnectBy: string | null;
|
|
49
|
+
/** When it was first connected. */
|
|
22
50
|
createdAt: string;
|
|
23
51
|
}
|
|
24
52
|
export type ConnectSessionStatus = 'pending' | 'connected' | 'failed' | 'expired';
|
package/dist/core/types.js
CHANGED
|
@@ -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'];
|
package/dist/element/copy.d.ts
CHANGED
|
@@ -1,39 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
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';
|
package/dist/element/copy.js
CHANGED
|
@@ -1,32 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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';
|
package/dist/element/dom.d.ts
CHANGED
|
@@ -3,21 +3,42 @@
|
|
|
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
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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,
|
|
27
|
+
export declare function mark(path: string, options?: {
|
|
28
|
+
viewBox?: string;
|
|
29
|
+
className?: string;
|
|
30
|
+
part?: string;
|
|
31
|
+
size?: number;
|
|
32
|
+
}): SVGSVGElement;
|
|
33
|
+
/**
|
|
34
|
+
* A QR code of `text` as one SVG path of dark modules on a light square, named `label` for a screen reader. It stays dark
|
|
35
|
+
* on light in a dark theme too, as cameras need; `--flung-qr-size` sets its size.
|
|
36
|
+
*/
|
|
37
|
+
export declare function qrCode(text: string, label: string): SVGSVGElement;
|
|
38
|
+
/**
|
|
39
|
+
* An account's picture: an `<img>` that sends no referrer, loads lazily and has an empty `alt` (the name is beside it),
|
|
40
|
+
* or a circle with the account's initial when there is no picture or it does not load. `avatarUrl` is only ever an
|
|
41
|
+
* `https` address (the controller drops anything else).
|
|
42
|
+
*/
|
|
43
|
+
export declare function avatar(item: AccountItem): HTMLElement;
|
|
23
44
|
export {};
|
package/dist/element/dom.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
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 { encodeQr } from '../core/qr.js';
|
|
6
7
|
/** An HTML element of the element's own. `true` sets an empty attribute; `false` and `undefined` set none. */
|
|
7
8
|
export function html(tag, attributes = {}, ...children) {
|
|
8
9
|
const element = document.createElement(tag);
|
|
@@ -16,22 +17,40 @@ export function html(tag, attributes = {}, ...children) {
|
|
|
16
17
|
element.append(child);
|
|
17
18
|
return element;
|
|
18
19
|
}
|
|
19
|
-
/** How an account is named in the panel: its username, or its platform id when it has none. */
|
|
20
|
+
/** How an account is named in the panel's messages: its username, or its platform id when it has none. */
|
|
20
21
|
export function displayName(account) {
|
|
21
22
|
return account.username ?? account.platformUserId;
|
|
22
23
|
}
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
/**
|
|
25
|
+
* A button that calls `onClick`, with a `data-key` so focus can be put back on it after the panel is drawn again. Its
|
|
26
|
+
* `part` is `button`, `button-<variant>` and `button-<role>`, so a page can style every button, a kind, or one of them.
|
|
27
|
+
*/
|
|
28
|
+
export function button(text, key, role, onClick, options = {}) {
|
|
29
|
+
const variant = options.variant ?? 'secondary';
|
|
30
|
+
const element = html('button', {
|
|
31
|
+
type: 'button',
|
|
32
|
+
class: variant,
|
|
33
|
+
part: `button button-${variant} button-${role}`,
|
|
34
|
+
'data-key': key,
|
|
35
|
+
disabled: options.disabled === true,
|
|
36
|
+
'aria-label': options.label,
|
|
37
|
+
// A button that opens a question inside the panel says whether it is open, and which question.
|
|
38
|
+
'aria-expanded': options.expanded === undefined ? undefined : String(options.expanded),
|
|
39
|
+
'aria-controls': options.controls,
|
|
40
|
+
}, text);
|
|
27
41
|
element.addEventListener('click', onClick);
|
|
28
42
|
return element;
|
|
29
43
|
}
|
|
30
44
|
/** 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,
|
|
45
|
+
export function mark(path, options = {}) {
|
|
32
46
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
33
|
-
svg.setAttribute('viewBox', viewBox);
|
|
34
|
-
svg.setAttribute('class', className);
|
|
47
|
+
svg.setAttribute('viewBox', options.viewBox ?? '0 0 24 24');
|
|
48
|
+
svg.setAttribute('class', options.className ?? 'mark');
|
|
49
|
+
svg.setAttribute('part', options.part ?? 'mark');
|
|
50
|
+
// Presentation attributes: any stylesheet overrides them, and an unstyled panel still draws a sensible size.
|
|
51
|
+
svg.setAttribute('width', String(options.size ?? 20));
|
|
52
|
+
svg.setAttribute('height', String(options.size ?? 20));
|
|
53
|
+
svg.setAttribute('fill', 'currentColor');
|
|
35
54
|
svg.setAttribute('aria-hidden', 'true');
|
|
36
55
|
svg.setAttribute('focusable', 'false');
|
|
37
56
|
const shape = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
@@ -39,3 +58,53 @@ export function mark(path, viewBox = '0 0 24 24', className = 'mark') {
|
|
|
39
58
|
svg.append(shape);
|
|
40
59
|
return svg;
|
|
41
60
|
}
|
|
61
|
+
/** The light modules kept around a QR code so a camera finds its edges (the standard asks for four; three reads well on a screen). */
|
|
62
|
+
const QR_QUIET = 3;
|
|
63
|
+
/**
|
|
64
|
+
* A QR code of `text` as one SVG path of dark modules on a light square, named `label` for a screen reader. It stays dark
|
|
65
|
+
* on light in a dark theme too, as cameras need; `--flung-qr-size` sets its size.
|
|
66
|
+
*/
|
|
67
|
+
export function qrCode(text, label) {
|
|
68
|
+
const code = encodeQr(text);
|
|
69
|
+
const size = code.size + QR_QUIET * 2;
|
|
70
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
71
|
+
svg.setAttribute('viewBox', `0 0 ${size} ${size}`);
|
|
72
|
+
svg.setAttribute('class', 'qr');
|
|
73
|
+
svg.setAttribute('part', 'qr');
|
|
74
|
+
svg.setAttribute('role', 'img');
|
|
75
|
+
svg.setAttribute('aria-label', label);
|
|
76
|
+
svg.setAttribute('shape-rendering', 'crispEdges');
|
|
77
|
+
// Presentation attributes, as `mark` sets: an unstyled panel still draws a code a camera reads.
|
|
78
|
+
svg.setAttribute('width', '132');
|
|
79
|
+
svg.setAttribute('height', '132');
|
|
80
|
+
const ground = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
81
|
+
ground.setAttribute('width', String(size));
|
|
82
|
+
ground.setAttribute('height', String(size));
|
|
83
|
+
ground.setAttribute('fill', '#ffffff');
|
|
84
|
+
const modules = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
85
|
+
const runs = [];
|
|
86
|
+
code.modules.forEach((row, y) => {
|
|
87
|
+
row.forEach((dark, x) => {
|
|
88
|
+
if (dark)
|
|
89
|
+
runs.push(`M${x + QR_QUIET} ${y + QR_QUIET}h1v1h-1z`);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
modules.setAttribute('d', runs.join(''));
|
|
93
|
+
modules.setAttribute('fill', '#000000');
|
|
94
|
+
svg.append(ground, modules);
|
|
95
|
+
return svg;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* An account's picture: an `<img>` that sends no referrer, loads lazily and has an empty `alt` (the name is beside it),
|
|
99
|
+
* or a circle with the account's initial when there is no picture or it does not load. `avatarUrl` is only ever an
|
|
100
|
+
* `https` address (the controller drops anything else).
|
|
101
|
+
*/
|
|
102
|
+
export function avatar(item) {
|
|
103
|
+
const initial = html('span', { class: 'avatar initial', part: 'avatar avatar-initial', 'aria-hidden': 'true' }, item.initial);
|
|
104
|
+
if (item.avatarUrl === null)
|
|
105
|
+
return initial;
|
|
106
|
+
const image = html('img', { class: 'avatar', part: 'avatar avatar-image', alt: '', referrerpolicy: 'no-referrer', loading: 'lazy', decoding: 'async', width: 32, height: 32 });
|
|
107
|
+
image.addEventListener('error', () => image.replaceWith(initial), { once: true });
|
|
108
|
+
image.src = item.avatarUrl;
|
|
109
|
+
return image;
|
|
110
|
+
}
|