@tineira/apc-web 0.7.1 → 0.9.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 +2 -0
- package/dist/agents-bar.d.ts +8 -5
- package/dist/agents-bar.js +59 -39
- package/dist/direct-agents.d.ts +5 -2
- package/dist/direct-agents.js +32 -22
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/pin-badge.d.ts +4 -1
- package/dist/pin-badge.js +18 -8
- package/dist/pinning.d.ts +27 -3
- package/dist/pinning.js +57 -29
- package/dist/strings.d.ts +133 -0
- package/dist/strings.js +126 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -27,4 +27,6 @@ const stop = enablePinning({ onPin: (grantId, target) => savePin(grantId, target
|
|
|
27
27
|
|
|
28
28
|
`onPin` gets the agent's grant and a `FocusTarget` (`{ kind, id, part, params }`); your server keeps it as that grant's pin and gives it to the agent ([`@tineira/apc-server`](https://www.npmjs.com/package/@tineira/apc-server)).
|
|
29
29
|
|
|
30
|
+
Every string it draws can be passed by your app (`strings` on each component; see "Localize the page" in the docs), with the English as default.
|
|
31
|
+
|
|
30
32
|
Source, the worked examples (Pulse and Signals) and the changelog: https://github.com/tineira/agent-pointed-context. MIT.
|
package/dist/agents-bar.d.ts
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* per browser). On by default; an app turns it off with `dockHint = null` or `dock-hint="off"`.
|
|
31
31
|
*/
|
|
32
32
|
import { type ApcProfile } from "@tineira/apc-protocol";
|
|
33
|
+
import { type BarStrings } from "./strings.js";
|
|
33
34
|
export type AgentState = "off" | "idle" | "waiting" | "busy" | "seen";
|
|
34
35
|
export interface BarAgent {
|
|
35
36
|
/** The app's id for the agent (its grant): what pins name. */
|
|
@@ -65,14 +66,14 @@ export declare function mergePins<P extends {
|
|
|
65
66
|
label: string;
|
|
66
67
|
seen: boolean;
|
|
67
68
|
here: boolean;
|
|
68
|
-
}>(pins: ReadonlyArray<P | null | undefined>): P | null;
|
|
69
|
+
}>(pins: ReadonlyArray<P | null | undefined>, strings?: Partial<BarStrings> | null): P | null;
|
|
69
70
|
/** The hint that offers the dock to a Direct user, and where it leads. */
|
|
70
71
|
export declare const DOCK_HINT: {
|
|
71
|
-
readonly text:
|
|
72
|
+
readonly text: string;
|
|
72
73
|
readonly url: "https://apc.tineira.com/dock";
|
|
73
74
|
};
|
|
74
75
|
/** The words for the path an agent came by, for its tag. Null when the app does not say. */
|
|
75
|
-
export declare function pathLabel(paths: readonly ApcProfile[] | undefined): {
|
|
76
|
+
export declare function pathLabel(paths: readonly ApcProfile[] | undefined, given?: Partial<BarStrings> | null): {
|
|
76
77
|
text: string;
|
|
77
78
|
title: string;
|
|
78
79
|
} | null;
|
|
@@ -84,14 +85,14 @@ export declare function isMobileViewport(): boolean;
|
|
|
84
85
|
*/
|
|
85
86
|
export declare function barCanCollapse(agents: readonly BarAgent[], mobile?: boolean): boolean;
|
|
86
87
|
/** Who is reading, for the collapsed indicator: the agents that connected. */
|
|
87
|
-
export declare function readingSummary(agents: readonly BarAgent[]): string;
|
|
88
|
+
export declare function readingSummary(agents: readonly BarAgent[], given?: Partial<BarStrings> | null): string;
|
|
88
89
|
/** The dock hint shows while an agent that came only directly is connected, unless the app turned it off or the person dismissed it. */
|
|
89
90
|
export declare function showDockHint(agents: readonly BarAgent[], options: {
|
|
90
91
|
enabled: boolean;
|
|
91
92
|
dismissed: boolean;
|
|
92
93
|
}): boolean;
|
|
93
94
|
/** The words a mark, its dot and its revoke button say, for tooltips and screen readers. Pure, so it is tested. */
|
|
94
|
-
export declare function agentLabels(a: BarAgent): {
|
|
95
|
+
export declare function agentLabels(a: BarAgent, given?: Partial<BarStrings> | null): {
|
|
95
96
|
mark: string;
|
|
96
97
|
dot: string | null;
|
|
97
98
|
revoke: string;
|
|
@@ -107,4 +108,6 @@ export interface ApcAgentsElement extends HTMLElement {
|
|
|
107
108
|
text: string;
|
|
108
109
|
url: string;
|
|
109
110
|
} | null;
|
|
111
|
+
/** The bar's strings (TIN-668): set the keys you translate; the rest stay English. */
|
|
112
|
+
strings: Partial<BarStrings> | null;
|
|
110
113
|
}
|
package/dist/agents-bar.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
*/
|
|
32
32
|
import { APC_MOBILE_MEDIA } from "@tineira/apc-protocol";
|
|
33
33
|
import { APC_ARMED_EVENT } from "./pinning.js";
|
|
34
|
+
import { DEFAULT_BAR_STRINGS, resolveStrings } from "./strings.js";
|
|
34
35
|
/** How long the first click on ⊘ waits for the second before it lets go. */
|
|
35
36
|
export const REVOKE_CONFIRM_MS = 4000;
|
|
36
37
|
/**
|
|
@@ -39,23 +40,24 @@ export const REVOKE_CONFIRM_MS = 4000;
|
|
|
39
40
|
* target once), unread until both are read, and "here" only when both targets are on screen (so the
|
|
40
41
|
* dot still opens the one that is not). No pin at all is null.
|
|
41
42
|
*/
|
|
42
|
-
export function mergePins(pins) {
|
|
43
|
+
export function mergePins(pins, strings) {
|
|
43
44
|
const some = pins.filter((p) => !!p);
|
|
44
45
|
if (some.length < 2)
|
|
45
46
|
return some[0] ?? null;
|
|
46
|
-
return { ...some[0], label: [...new Set(some.map((p) => p.label))]
|
|
47
|
+
return { ...some[0], label: resolveStrings(DEFAULT_BAR_STRINGS, strings).joinPins([...new Set(some.map((p) => p.label))]), seen: some.every((p) => p.seen), here: some.every((p) => p.here) };
|
|
47
48
|
}
|
|
48
49
|
/** The hint that offers the dock to a Direct user, and where it leads. */
|
|
49
|
-
export const DOCK_HINT = { text:
|
|
50
|
+
export const DOCK_HINT = { text: DEFAULT_BAR_STRINGS.dockHint, url: "https://apc.tineira.com/dock" };
|
|
50
51
|
const COLLAPSED_KEY = "apc-agents.collapsed";
|
|
51
52
|
const HINT_KEY = "apc-agents.dock-hint";
|
|
52
53
|
/** The words for the path an agent came by, for its tag. Null when the app does not say. */
|
|
53
|
-
export function pathLabel(paths) {
|
|
54
|
+
export function pathLabel(paths, given) {
|
|
54
55
|
if (!paths?.length)
|
|
55
56
|
return null;
|
|
57
|
+
const strings = resolveStrings(DEFAULT_BAR_STRINGS, given);
|
|
56
58
|
if (paths.includes("direct") && paths.includes("dock"))
|
|
57
|
-
return { text:
|
|
58
|
-
return paths[0] === "direct" ? { text:
|
|
59
|
+
return { text: strings.pathBoth, title: strings.pathBothTitle };
|
|
60
|
+
return paths[0] === "direct" ? { text: strings.pathDirect, title: strings.pathDirectTitle } : { text: strings.pathDock, title: strings.pathDockTitle };
|
|
59
61
|
}
|
|
60
62
|
/** Whether this window is a phone or any touch viewport (APC 0.6): a coarse pointer or a narrow width. False where there is no window. */
|
|
61
63
|
export function isMobileViewport() {
|
|
@@ -69,13 +71,10 @@ export function barCanCollapse(agents, mobile = false) {
|
|
|
69
71
|
return !mobile && agents.length > 0 && agents.every((a) => !!a.paths?.length && !a.paths.includes("direct"));
|
|
70
72
|
}
|
|
71
73
|
/** Who is reading, for the collapsed indicator: the agents that connected. */
|
|
72
|
-
export function readingSummary(agents) {
|
|
74
|
+
export function readingSummary(agents, given) {
|
|
75
|
+
const strings = resolveStrings(DEFAULT_BAR_STRINGS, given);
|
|
73
76
|
const names = agents.filter((a) => a.state !== "off").map((a) => a.name);
|
|
74
|
-
|
|
75
|
-
return "No agent is reading";
|
|
76
|
-
if (names.length === 1)
|
|
77
|
-
return `${names[0]} is reading`;
|
|
78
|
-
return `${names.slice(0, -1).join(", ")} and ${names.at(-1)} are reading`;
|
|
77
|
+
return names.length ? strings.reading(names) : strings.nobodyReading;
|
|
79
78
|
}
|
|
80
79
|
/** The dock hint shows while an agent that came only directly is connected, unless the app turned it off or the person dismissed it. */
|
|
81
80
|
export function showDockHint(agents, options) {
|
|
@@ -163,15 +162,15 @@ html.apc-bar-fixed body { padding-bottom: var(--apc-bar-height, 0px); }
|
|
|
163
162
|
/** A small robot, for an agent with no icon. */
|
|
164
163
|
const BOT = '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M11 2h2v2.07A7 7 0 0 1 19 11v7a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3v-7a7 7 0 0 1 6-6.93V2Zm-2.5 10a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm7 0a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3ZM2 12h2v5H2v-5Zm18 0h2v5h-2v-5Z"/></svg>';
|
|
165
164
|
/** The words a mark, its dot and its revoke button say, for tooltips and screen readers. Pure, so it is tested. */
|
|
166
|
-
export function agentLabels(a) {
|
|
167
|
-
const
|
|
165
|
+
export function agentLabels(a, given) {
|
|
166
|
+
const strings = resolveStrings(DEFAULT_BAR_STRINGS, given);
|
|
167
|
+
const mark = strings.markTitle(a.name, a.state !== "off");
|
|
168
168
|
const both = !!a.paths?.includes("direct") && !!a.paths.includes("dock");
|
|
169
|
-
const revoke =
|
|
170
|
-
const revokeArmed =
|
|
169
|
+
const revoke = strings.revokeTitle(a.name, both);
|
|
170
|
+
const revokeArmed = strings.revokeConfirmTitle(a.name);
|
|
171
171
|
if (!a.pin)
|
|
172
172
|
return { mark, dot: null, revoke, revokeArmed };
|
|
173
|
-
|
|
174
|
-
return { mark, dot: `Pinned to ${a.pin.label}: ${read}${a.pin.here ? "" : ". Click to open it."}`, revoke, revokeArmed };
|
|
173
|
+
return { mark, dot: strings.pinTitle(a.pin.label, a.pin.seen, a.pin.here), revoke, revokeArmed };
|
|
175
174
|
}
|
|
176
175
|
/** The room each fixed bar takes at the bottom of the page: the page keeps the largest, as --apc-bar-height and padding on <body>. */
|
|
177
176
|
const reserved = new Map();
|
|
@@ -202,7 +201,12 @@ export function defineAgentBar(tag = "apc-agents") {
|
|
|
202
201
|
armed = new Map();
|
|
203
202
|
/** The app draws an agent's icon into this element itself (for example with its own logos). Called on every update. */
|
|
204
203
|
renderIcon = null;
|
|
205
|
-
hint
|
|
204
|
+
/** undefined: the default hint, in the bar's strings; null: off. */
|
|
205
|
+
hint = undefined;
|
|
206
|
+
words = DEFAULT_BAR_STRINGS;
|
|
207
|
+
given = null;
|
|
208
|
+
/** Bumped when the strings change, so every entry is drawn again. */
|
|
209
|
+
stringsVersion = 0;
|
|
206
210
|
/** The collapsed indicator's icons, kept per agent like the marks' (an app's renderIcon may keep state in them). */
|
|
207
211
|
miniIcons = new Map();
|
|
208
212
|
get agents() {
|
|
@@ -214,12 +218,26 @@ export function defineAgentBar(tag = "apc-agents") {
|
|
|
214
218
|
}
|
|
215
219
|
/** The hint that offers the dock to a Direct user; null turns it off. */
|
|
216
220
|
get dockHint() {
|
|
217
|
-
|
|
221
|
+
if (this.getAttribute("dock-hint") === "off" || this.hint === null)
|
|
222
|
+
return null;
|
|
223
|
+
return this.hint ?? { text: this.words.dockHint, url: DOCK_HINT.url };
|
|
218
224
|
}
|
|
219
225
|
set dockHint(next) {
|
|
220
226
|
this.hint = next;
|
|
221
227
|
this.render();
|
|
222
228
|
}
|
|
229
|
+
/** Every string the bar shows (TIN-668): the keys you set, over the English defaults. */
|
|
230
|
+
get strings() {
|
|
231
|
+
return this.given;
|
|
232
|
+
}
|
|
233
|
+
set strings(next) {
|
|
234
|
+
this.given = next;
|
|
235
|
+
this.words = resolveStrings(DEFAULT_BAR_STRINGS, next);
|
|
236
|
+
this.stringsVersion++;
|
|
237
|
+
if (this.getAttribute("data-apc-label") === "default")
|
|
238
|
+
this.setAttribute("aria-label", this.words.toolbar);
|
|
239
|
+
this.render();
|
|
240
|
+
}
|
|
223
241
|
/** Mobile (APC 0.6): the viewport is a phone's or a touch one's; the bar then never collapses. */
|
|
224
242
|
media = null;
|
|
225
243
|
resizer = null;
|
|
@@ -268,8 +286,10 @@ export function defineAgentBar(tag = "apc-agents") {
|
|
|
268
286
|
}
|
|
269
287
|
if (!this.hasAttribute("role"))
|
|
270
288
|
this.setAttribute("role", "toolbar");
|
|
271
|
-
if (!this.hasAttribute("aria-label"))
|
|
272
|
-
this.setAttribute("aria-label",
|
|
289
|
+
if (!this.hasAttribute("aria-label")) {
|
|
290
|
+
this.setAttribute("aria-label", this.words.toolbar);
|
|
291
|
+
this.setAttribute("data-apc-label", "default");
|
|
292
|
+
}
|
|
273
293
|
this.render();
|
|
274
294
|
}
|
|
275
295
|
emit(type, id) {
|
|
@@ -334,8 +354,8 @@ export function defineAgentBar(tag = "apc-agents") {
|
|
|
334
354
|
fold.type = "button";
|
|
335
355
|
fold.className = "apc-agents__collapse apc-agents__extra";
|
|
336
356
|
fold.textContent = "‹";
|
|
337
|
-
fold.title =
|
|
338
|
-
fold.setAttribute("aria-label",
|
|
357
|
+
fold.title = this.words.collapseTitle;
|
|
358
|
+
fold.setAttribute("aria-label", this.words.collapseAria);
|
|
339
359
|
fold.addEventListener("click", () => {
|
|
340
360
|
store(COLLAPSED_KEY, "1");
|
|
341
361
|
this.render();
|
|
@@ -352,9 +372,9 @@ export function defineAgentBar(tag = "apc-agents") {
|
|
|
352
372
|
const button = document.createElement("button");
|
|
353
373
|
button.type = "button";
|
|
354
374
|
button.className = "apc-agents__mini apc-agents__extra";
|
|
355
|
-
const summary = readingSummary(this.list);
|
|
356
|
-
button.title =
|
|
357
|
-
button.setAttribute("aria-label",
|
|
375
|
+
const summary = readingSummary(this.list, this.words);
|
|
376
|
+
button.title = this.words.showAgents(summary);
|
|
377
|
+
button.setAttribute("aria-label", this.words.showAgents(summary));
|
|
358
378
|
for (const a of reading.slice(0, 4)) {
|
|
359
379
|
let icon = this.miniIcons.get(a.id);
|
|
360
380
|
if (!icon) {
|
|
@@ -374,7 +394,7 @@ export function defineAgentBar(tag = "apc-agents") {
|
|
|
374
394
|
icon.innerHTML = BOT;
|
|
375
395
|
button.append(icon);
|
|
376
396
|
}
|
|
377
|
-
button.append(document.createTextNode(reading.length ?
|
|
397
|
+
button.append(document.createTextNode(reading.length ? this.words.readingCount(reading.length) : this.words.nobodyReadingShort));
|
|
378
398
|
button.addEventListener("click", () => {
|
|
379
399
|
store(COLLAPSED_KEY, null);
|
|
380
400
|
this.render();
|
|
@@ -397,15 +417,15 @@ export function defineAgentBar(tag = "apc-agents") {
|
|
|
397
417
|
const close = document.createElement("button");
|
|
398
418
|
close.type = "button";
|
|
399
419
|
close.textContent = "×";
|
|
400
|
-
close.setAttribute("aria-label",
|
|
420
|
+
close.setAttribute("aria-label", this.words.dockHintDismiss);
|
|
401
421
|
close.addEventListener("click", dismiss);
|
|
402
422
|
box.append(link, close);
|
|
403
423
|
return box;
|
|
404
424
|
}
|
|
405
425
|
fill(node, a) {
|
|
406
|
-
const labels = agentLabels(a);
|
|
426
|
+
const labels = agentLabels(a, this.words);
|
|
407
427
|
const armed = this.armed.has(a.id);
|
|
408
|
-
const key = JSON.stringify([a.name, a.state, a.pin, a.warning, a.restricted, typeof a.icon === "string" ? a.icon.length : null, armed, a.paths ?? null]);
|
|
428
|
+
const key = JSON.stringify([a.name, a.state, a.pin, a.warning, a.restricted, typeof a.icon === "string" ? a.icon.length : null, armed, a.paths ?? null, this.stringsVersion]);
|
|
409
429
|
if (this.renderIcon)
|
|
410
430
|
this.renderIcon(a, node.icon);
|
|
411
431
|
else if (node.key === "" || JSON.stringify(a.icon ?? null) !== node.icon.dataset.src) {
|
|
@@ -451,19 +471,19 @@ export function defineAgentBar(tag = "apc-agents") {
|
|
|
451
471
|
dot.type = "button";
|
|
452
472
|
dot.className = `apc-agents__dot${a.pin.seen ? " apc-agents__dot--seen" : ""}${a.pin.here ? "" : " apc-agents__dot--away"}`;
|
|
453
473
|
dot.title = labels.dot;
|
|
454
|
-
dot.setAttribute("aria-label",
|
|
474
|
+
dot.setAttribute("aria-label", this.words.pinAria(a.name, a.pin.label, a.pin.seen));
|
|
455
475
|
dot.addEventListener("click", () => !a.pin?.here && this.emit("apc-open", a.id));
|
|
456
476
|
const unpin = document.createElement("button");
|
|
457
477
|
unpin.type = "button";
|
|
458
478
|
unpin.className = "apc-agents__unpin";
|
|
459
479
|
unpin.textContent = "×";
|
|
460
|
-
unpin.setAttribute("aria-label",
|
|
480
|
+
unpin.setAttribute("aria-label", this.words.unpinAria(a.name));
|
|
461
481
|
unpin.addEventListener("click", (e) => e.isTrusted && this.emit("apc-unpin", a.id));
|
|
462
482
|
pin.append(dot, unpin);
|
|
463
483
|
parts.push(pin);
|
|
464
484
|
}
|
|
465
485
|
// Which path it came by; a linked agent can be shown as two again.
|
|
466
|
-
const path = pathLabel(a.paths);
|
|
486
|
+
const path = pathLabel(a.paths, this.words);
|
|
467
487
|
if (path) {
|
|
468
488
|
const tag = document.createElement("span");
|
|
469
489
|
tag.className = "apc-agents__path";
|
|
@@ -474,9 +494,9 @@ export function defineAgentBar(tag = "apc-agents") {
|
|
|
474
494
|
const unlink = document.createElement("button");
|
|
475
495
|
unlink.type = "button";
|
|
476
496
|
unlink.className = "apc-agents__unlink";
|
|
477
|
-
unlink.textContent =
|
|
478
|
-
unlink.title =
|
|
479
|
-
unlink.setAttribute("aria-label",
|
|
497
|
+
unlink.textContent = this.words.unlink;
|
|
498
|
+
unlink.title = this.words.unlinkTitle(a.name);
|
|
499
|
+
unlink.setAttribute("aria-label", this.words.unlinkAria(a.name));
|
|
480
500
|
unlink.addEventListener("click", (e) => e.isTrusted && this.emit("apc-unlink", a.id));
|
|
481
501
|
parts.push(unlink);
|
|
482
502
|
}
|
|
@@ -485,9 +505,9 @@ export function defineAgentBar(tag = "apc-agents") {
|
|
|
485
505
|
const revoke = document.createElement("button");
|
|
486
506
|
revoke.type = "button";
|
|
487
507
|
revoke.className = `apc-agents__revoke${armed ? " apc-agents__revoke--armed" : ""}`;
|
|
488
|
-
revoke.textContent = armed ?
|
|
508
|
+
revoke.textContent = armed ? this.words.revokeConfirm : "⊘";
|
|
489
509
|
revoke.title = armed ? labels.revokeArmed : labels.revoke;
|
|
490
|
-
revoke.setAttribute("aria-label", armed ? labels.revokeArmed :
|
|
510
|
+
revoke.setAttribute("aria-label", armed ? labels.revokeArmed : this.words.revokeAria(a.name));
|
|
491
511
|
revoke.addEventListener("click", (e) => e.isTrusted && this.revokeClicked(a.id));
|
|
492
512
|
revoke.addEventListener("keydown", (e) => e.key === "Escape" && this.disarm(a.id));
|
|
493
513
|
parts.push(revoke);
|
package/dist/direct-agents.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* scoped by class prefix and themed through --apc-* custom properties. Set `agents` and `appName`;
|
|
9
9
|
* listen for `apc-add` (make an agent) and `apc-rename` (detail { id, name }).
|
|
10
10
|
*/
|
|
11
|
+
import { type DirectStrings } from "./strings.js";
|
|
11
12
|
export interface DirectAgent {
|
|
12
13
|
/** The app's id for the agent's grant. */
|
|
13
14
|
id: string;
|
|
@@ -27,13 +28,15 @@ export type DirectAgentsEvent = "apc-add" | "apc-rename";
|
|
|
27
28
|
* asked in the chat, so it asks for that first, under a name the person will recognise, and for the
|
|
28
29
|
* steps only if the agent cannot.
|
|
29
30
|
*/
|
|
30
|
-
export declare function connectPrompt(url: string, appName: string): string;
|
|
31
|
+
export declare function connectPrompt(url: string, appName: string, strings?: Partial<DirectStrings> | null): string;
|
|
31
32
|
/** The words of one agent's line. Pure, so it is tested. */
|
|
32
|
-
export declare function directAgentStatus(a: DirectAgent): string;
|
|
33
|
+
export declare function directAgentStatus(a: DirectAgent, strings?: Partial<DirectStrings> | null): string;
|
|
33
34
|
/** Defines `<apc-direct-agents>` where custom elements exist (a browser), so importing this module in Node is safe. */
|
|
34
35
|
export declare function defineDirectAgents(tag?: string): void;
|
|
35
36
|
/** The element's type, for apps that hold a reference to it. */
|
|
36
37
|
export interface ApcDirectAgentsElement extends HTMLElement {
|
|
37
38
|
agents: DirectAgent[];
|
|
38
39
|
appName: string;
|
|
40
|
+
/** The list's strings (TIN-668): set the keys you translate; the rest stay English. */
|
|
41
|
+
strings: Partial<DirectStrings> | null;
|
|
39
42
|
}
|
package/dist/direct-agents.js
CHANGED
|
@@ -8,20 +8,18 @@
|
|
|
8
8
|
* scoped by class prefix and themed through --apc-* custom properties. Set `agents` and `appName`;
|
|
9
9
|
* listen for `apc-add` (make an agent) and `apc-rename` (detail { id, name }).
|
|
10
10
|
*/
|
|
11
|
+
import { DEFAULT_DIRECT_STRINGS, resolveStrings } from "./strings.js";
|
|
11
12
|
/**
|
|
12
13
|
* What a person pastes to their own agent to connect it. Many agents can add a server themselves when
|
|
13
14
|
* asked in the chat, so it asks for that first, under a name the person will recognise, and for the
|
|
14
15
|
* steps only if the agent cannot.
|
|
15
16
|
*/
|
|
16
|
-
export function connectPrompt(url, appName) {
|
|
17
|
-
return
|
|
17
|
+
export function connectPrompt(url, appName, strings) {
|
|
18
|
+
return resolveStrings(DEFAULT_DIRECT_STRINGS, strings).prompt(url, appName);
|
|
18
19
|
}
|
|
19
20
|
/** The words of one agent's line. Pure, so it is tested. */
|
|
20
|
-
export function directAgentStatus(a) {
|
|
21
|
-
|
|
22
|
-
return "not connected yet";
|
|
23
|
-
const as = a.client ? `connected as ${a.client}` : "connected";
|
|
24
|
-
return a.linked ? `${as}, also through your dock` : as;
|
|
21
|
+
export function directAgentStatus(a, strings) {
|
|
22
|
+
return resolveStrings(DEFAULT_DIRECT_STRINGS, strings).status(a);
|
|
25
23
|
}
|
|
26
24
|
const STYLE_ID = "apc-direct-style";
|
|
27
25
|
const CSS = `
|
|
@@ -43,7 +41,7 @@ apc-direct-agents { display: flex; flex-direction: column; gap: 10px; color: var
|
|
|
43
41
|
.apc-direct__add { align-self: flex-start; }
|
|
44
42
|
.apc-direct__add:hover, .apc-direct__rename button:hover { border-color: var(--apc-accent, #b85209); }
|
|
45
43
|
`;
|
|
46
|
-
function copyField(text, label) {
|
|
44
|
+
function copyField(text, label, copied) {
|
|
47
45
|
const box = document.createElement("div");
|
|
48
46
|
box.className = "apc-direct__copy";
|
|
49
47
|
const code = document.createElement("code");
|
|
@@ -53,7 +51,7 @@ function copyField(text, label) {
|
|
|
53
51
|
button.textContent = label;
|
|
54
52
|
button.addEventListener("click", () => {
|
|
55
53
|
void navigator.clipboard?.writeText(text).then(() => {
|
|
56
|
-
button.textContent =
|
|
54
|
+
button.textContent = copied;
|
|
57
55
|
setTimeout(() => (button.textContent = label), 1500);
|
|
58
56
|
}, () => undefined);
|
|
59
57
|
});
|
|
@@ -66,7 +64,9 @@ export function defineDirectAgents(tag = "apc-direct-agents") {
|
|
|
66
64
|
return;
|
|
67
65
|
class ApcDirectAgents extends HTMLElement {
|
|
68
66
|
list = [];
|
|
69
|
-
name =
|
|
67
|
+
name = null;
|
|
68
|
+
words = DEFAULT_DIRECT_STRINGS;
|
|
69
|
+
given = null;
|
|
70
70
|
/** Agents whose connection is unfolded; an agent not connected yet is unfolded until it connects. */
|
|
71
71
|
open = new Set();
|
|
72
72
|
shut = new Set();
|
|
@@ -80,12 +80,21 @@ export function defineDirectAgents(tag = "apc-direct-agents") {
|
|
|
80
80
|
this.render();
|
|
81
81
|
}
|
|
82
82
|
get appName() {
|
|
83
|
-
return this.name;
|
|
83
|
+
return this.name ?? this.words.thisApp;
|
|
84
84
|
}
|
|
85
85
|
set appName(next) {
|
|
86
86
|
this.name = next;
|
|
87
87
|
this.render();
|
|
88
88
|
}
|
|
89
|
+
/** Every string the list shows (TIN-668): the keys you set, over the English defaults. */
|
|
90
|
+
get strings() {
|
|
91
|
+
return this.given;
|
|
92
|
+
}
|
|
93
|
+
set strings(next) {
|
|
94
|
+
this.given = next;
|
|
95
|
+
this.words = resolveStrings(DEFAULT_DIRECT_STRINGS, next);
|
|
96
|
+
this.render();
|
|
97
|
+
}
|
|
89
98
|
connectedCallback() {
|
|
90
99
|
if (!document.getElementById(STYLE_ID)) {
|
|
91
100
|
const style = document.createElement("style");
|
|
@@ -104,6 +113,7 @@ export function defineDirectAgents(tag = "apc-direct-agents") {
|
|
|
104
113
|
render() {
|
|
105
114
|
if (!this.isConnected)
|
|
106
115
|
return;
|
|
116
|
+
const w = this.words;
|
|
107
117
|
const rows = this.list.map((a) => {
|
|
108
118
|
const row = document.createElement("div");
|
|
109
119
|
row.className = "apc-direct__agent";
|
|
@@ -117,14 +127,14 @@ export function defineDirectAgents(tag = "apc-direct-agents") {
|
|
|
117
127
|
input.value = this.draft;
|
|
118
128
|
input.addEventListener("input", () => (this.draft = input.value));
|
|
119
129
|
input.maxLength = 40;
|
|
120
|
-
input.setAttribute("aria-label",
|
|
121
|
-
input.placeholder =
|
|
130
|
+
input.setAttribute("aria-label", w.nameLabel);
|
|
131
|
+
input.placeholder = w.namePlaceholder;
|
|
122
132
|
const save = document.createElement("button");
|
|
123
133
|
save.type = "submit";
|
|
124
|
-
save.textContent =
|
|
134
|
+
save.textContent = w.save;
|
|
125
135
|
const cancel = document.createElement("button");
|
|
126
136
|
cancel.type = "button";
|
|
127
|
-
cancel.textContent =
|
|
137
|
+
cancel.textContent = w.cancel;
|
|
128
138
|
cancel.addEventListener("click", () => {
|
|
129
139
|
this.editing = null;
|
|
130
140
|
this.render();
|
|
@@ -149,11 +159,11 @@ export function defineDirectAgents(tag = "apc-direct-agents") {
|
|
|
149
159
|
name.textContent = a.name;
|
|
150
160
|
const status = document.createElement("span");
|
|
151
161
|
status.className = "apc-direct__status";
|
|
152
|
-
status.textContent = directAgentStatus(a);
|
|
162
|
+
status.textContent = directAgentStatus(a, w);
|
|
153
163
|
const rename = document.createElement("button");
|
|
154
164
|
rename.type = "button";
|
|
155
165
|
rename.className = "apc-direct__link";
|
|
156
|
-
rename.textContent =
|
|
166
|
+
rename.textContent = w.rename;
|
|
157
167
|
rename.addEventListener("click", () => {
|
|
158
168
|
this.editing = a.id;
|
|
159
169
|
this.draft = a.name;
|
|
@@ -163,7 +173,7 @@ export function defineDirectAgents(tag = "apc-direct-agents") {
|
|
|
163
173
|
toggle.type = "button";
|
|
164
174
|
toggle.className = "apc-direct__link";
|
|
165
175
|
const open = this.isOpen(a);
|
|
166
|
-
toggle.textContent = open ?
|
|
176
|
+
toggle.textContent = open ? w.hideConnection : w.showConnection;
|
|
167
177
|
toggle.setAttribute("aria-expanded", String(open));
|
|
168
178
|
toggle.addEventListener("click", () => {
|
|
169
179
|
if (open) {
|
|
@@ -184,11 +194,11 @@ export function defineDirectAgents(tag = "apc-direct-agents") {
|
|
|
184
194
|
body.className = "apc-direct__body";
|
|
185
195
|
const urlHint = document.createElement("p");
|
|
186
196
|
urlHint.className = "apc-direct__hint";
|
|
187
|
-
urlHint.textContent =
|
|
197
|
+
urlHint.textContent = w.urlHint;
|
|
188
198
|
const askHint = document.createElement("p");
|
|
189
199
|
askHint.className = "apc-direct__hint";
|
|
190
|
-
askHint.textContent =
|
|
191
|
-
body.append(urlHint, copyField(a.url,
|
|
200
|
+
askHint.textContent = w.askHint;
|
|
201
|
+
body.append(urlHint, copyField(a.url, w.copy, w.copied), askHint, copyField(connectPrompt(a.url, this.appName, w), w.copyPrompt, w.copied));
|
|
192
202
|
row.append(body);
|
|
193
203
|
}
|
|
194
204
|
return row;
|
|
@@ -196,7 +206,7 @@ export function defineDirectAgents(tag = "apc-direct-agents") {
|
|
|
196
206
|
const add = document.createElement("button");
|
|
197
207
|
add.type = "button";
|
|
198
208
|
add.className = "apc-direct__add";
|
|
199
|
-
add.textContent = this.list.length ?
|
|
209
|
+
add.textContent = this.list.length ? w.addAnother : w.connectAgent;
|
|
200
210
|
add.addEventListener("click", () => this.emit("apc-add"));
|
|
201
211
|
this.replaceChildren(...rows, add);
|
|
202
212
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/pin-badge.d.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* script has no customElements, and it moves the badge with its own drag, since a moved reading is
|
|
19
19
|
* sent again from the page. Keep the two in step.
|
|
20
20
|
*/
|
|
21
|
+
import { type PinStrings } from "./strings.js";
|
|
21
22
|
export interface PinAgent {
|
|
22
23
|
/** The app's id for the agent (its grant): what the mark drags as. */
|
|
23
24
|
id: string;
|
|
@@ -30,7 +31,7 @@ export interface PinAgent {
|
|
|
30
31
|
/** Event, with detail { id }: the person removed the pin. */
|
|
31
32
|
export type PinBadgeEvent = "apc-unpin";
|
|
32
33
|
/** The words a badge says, for its tooltip and screen readers. Pure, so it is tested. */
|
|
33
|
-
export declare function pinBadgeLabels(a: Pick<PinAgent, "name" | "seen">): {
|
|
34
|
+
export declare function pinBadgeLabels(a: Pick<PinAgent, "name" | "seen">, given?: Partial<PinStrings> | null): {
|
|
34
35
|
title: string;
|
|
35
36
|
unpin: string;
|
|
36
37
|
};
|
|
@@ -42,4 +43,6 @@ export interface ApcPinElement extends HTMLElement {
|
|
|
42
43
|
readonly mark: HTMLElement | null;
|
|
43
44
|
readonly iconHost: HTMLElement | null;
|
|
44
45
|
renderIcon: ((agent: PinAgent, into: HTMLElement) => void) | null;
|
|
46
|
+
/** The badge's strings (TIN-668): set the keys you translate; the rest stay English. */
|
|
47
|
+
strings: Partial<PinStrings> | null;
|
|
45
48
|
}
|
package/dist/pin-badge.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* script has no customElements, and it moves the badge with its own drag, since a moved reading is
|
|
19
19
|
* sent again from the page. Keep the two in step.
|
|
20
20
|
*/
|
|
21
|
+
import { DEFAULT_PIN_STRINGS, resolveStrings } from "./strings.js";
|
|
21
22
|
const STYLE_ID = "apc-pin-style";
|
|
22
23
|
const CSS = `
|
|
23
24
|
apc-pin { position: relative; display: inline-block; width: var(--apc-pin-size, 28px); height: var(--apc-pin-size, 28px); vertical-align: middle; }
|
|
@@ -40,11 +41,9 @@ apc-pin.apc-pin--dragging { opacity: 0.35; }
|
|
|
40
41
|
/** A small robot, for an agent with no icon. */
|
|
41
42
|
const BOT = '<svg class="apc-pin__img" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M11 2h2v2.07A7 7 0 0 1 19 11v7a3 3 0 0 1-3 3H8a3 3 0 0 1-3-3v-7a7 7 0 0 1 6-6.93V2Zm-2.5 10a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm7 0a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3ZM2 12h2v5H2v-5Zm18 0h2v5h-2v-5Z"/></svg>';
|
|
42
43
|
/** The words a badge says, for its tooltip and screen readers. Pure, so it is tested. */
|
|
43
|
-
export function pinBadgeLabels(a) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
unpin: `Remove ${a.name}'s pin`,
|
|
47
|
-
};
|
|
44
|
+
export function pinBadgeLabels(a, given) {
|
|
45
|
+
const strings = resolveStrings(DEFAULT_PIN_STRINGS, given);
|
|
46
|
+
return { title: strings.title(a.name, a.seen), unpin: strings.unpinAria(a.name) };
|
|
48
47
|
}
|
|
49
48
|
/** The element's constructor, defined only where custom elements exist (a browser), so importing this module in Node is safe. */
|
|
50
49
|
export function definePinBadge(tag = "apc-pin") {
|
|
@@ -57,6 +56,17 @@ export function definePinBadge(tag = "apc-pin") {
|
|
|
57
56
|
iconKey = "";
|
|
58
57
|
stopDim = null;
|
|
59
58
|
draw = null;
|
|
59
|
+
words = DEFAULT_PIN_STRINGS;
|
|
60
|
+
given = null;
|
|
61
|
+
/** Every string the badge shows (TIN-668): the keys you set, over the English defaults. */
|
|
62
|
+
get strings() {
|
|
63
|
+
return this.given;
|
|
64
|
+
}
|
|
65
|
+
set strings(next) {
|
|
66
|
+
this.given = next;
|
|
67
|
+
this.words = resolveStrings(DEFAULT_PIN_STRINGS, next);
|
|
68
|
+
this.render();
|
|
69
|
+
}
|
|
60
70
|
/**
|
|
61
71
|
* The app draws the agent's icon into this element itself (for example with its own logos). Called on every update.
|
|
62
72
|
* A function that draws nothing says the app draws it elsewhere, into `iconHost` (React: a portal).
|
|
@@ -86,7 +96,7 @@ export function definePinBadge(tag = "apc-pin") {
|
|
|
86
96
|
connectedCallback() {
|
|
87
97
|
// A property set before the element was defined (a framework, a script) sits on the instance: take it in through the setter.
|
|
88
98
|
const own = this;
|
|
89
|
-
for (const key of ["agent", "renderIcon"]) {
|
|
99
|
+
for (const key of ["agent", "renderIcon", "strings"]) {
|
|
90
100
|
if (!Object.hasOwn(own, key))
|
|
91
101
|
continue;
|
|
92
102
|
const value = own[key];
|
|
@@ -146,12 +156,12 @@ export function definePinBadge(tag = "apc-pin") {
|
|
|
146
156
|
if (!this.parts || !a)
|
|
147
157
|
return;
|
|
148
158
|
const { mark, icon, check, unpin } = this.parts;
|
|
149
|
-
const labels = pinBadgeLabels(a);
|
|
159
|
+
const labels = pinBadgeLabels(a, this.words);
|
|
150
160
|
this.setAttribute("title", labels.title);
|
|
151
161
|
this.setAttribute("data-apc-pin-state", a.seen ? "seen" : "waiting");
|
|
152
162
|
mark.setAttribute("data-grant", a.id);
|
|
153
163
|
mark.setAttribute("role", "img");
|
|
154
|
-
mark.setAttribute("aria-label",
|
|
164
|
+
mark.setAttribute("aria-label", this.words.markAria(a.name, a.seen));
|
|
155
165
|
check.hidden = !a.seen;
|
|
156
166
|
unpin.setAttribute("aria-label", labels.unpin);
|
|
157
167
|
if (this.draw)
|
package/dist/pinning.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ApcRole, type FocusTarget } from "@tineira/apc-protocol";
|
|
2
|
+
import { type PinningStrings } from "./strings.js";
|
|
2
3
|
/** How long the page waits for its own apc.json: a held drop never waits longer. */
|
|
3
4
|
export declare const APC_JSON_LOAD_MS = 3000;
|
|
4
5
|
/**
|
|
@@ -57,6 +58,12 @@ export interface PinningOptions {
|
|
|
57
58
|
* A test that sends events of its own passes `() => true`; nothing in a page can reach this.
|
|
58
59
|
*/
|
|
59
60
|
trusted?: (event: Event) => boolean;
|
|
61
|
+
/**
|
|
62
|
+
* The words enablePinning shows the person (TIN-668): the tap-to-pin note, the answers to a refused
|
|
63
|
+
* drop, and the note for a dock agent dropped on a Direct-only app (`directOnlyNote`, `couldNotCheckNote`
|
|
64
|
+
* replace the protocol's English ones). The keys you leave out stay English.
|
|
65
|
+
*/
|
|
66
|
+
strings?: Partial<PinningStrings>;
|
|
60
67
|
}
|
|
61
68
|
/** The answer to a dock agent dropped on a Direct-only app: who it was, what to tell the person, and where they connect instead. */
|
|
62
69
|
export interface DockUnsupported {
|
|
@@ -67,13 +74,30 @@ export interface DockUnsupported {
|
|
|
67
74
|
connect: string | null;
|
|
68
75
|
/**
|
|
69
76
|
* `direct_only`: the app's apc.json lists no "dock". `unchecked`: the page could not read its own
|
|
70
|
-
* apc.json, so it cannot tell, and it pairs nothing it cannot check.
|
|
77
|
+
* apc.json, so it cannot tell, and it pairs nothing it cannot check. `refused` (TIN-756): the app offers
|
|
78
|
+
* Dock and answered the drag, then cannot place the drop (its `canTarget` said no, or it does not know the
|
|
79
|
+
* agent); `connect` is null and the app's handler need not send the person to its connect page.
|
|
71
80
|
*/
|
|
72
|
-
reason: "direct_only" | "unchecked";
|
|
81
|
+
reason: "direct_only" | "unchecked" | "refused";
|
|
73
82
|
/** `directOnlyNote(name)`, or `couldNotCheckNote(name)`: the app's own words, on its own page. */
|
|
74
83
|
message: string;
|
|
75
84
|
}
|
|
76
|
-
export declare function showDockUnsupported(answer: DockUnsupported): HTMLElement;
|
|
85
|
+
export declare function showDockUnsupported(answer: DockUnsupported, given?: Partial<PinningStrings> | null): HTMLElement;
|
|
86
|
+
/** What the person is told when an app answered a dock agent's drop and then cannot place it (TIN-756). */
|
|
87
|
+
export declare const REFUSED_NOTE: string;
|
|
88
|
+
export declare const UNKNOWN_AGENT_NOTE: string;
|
|
89
|
+
/** How a refused drop is told: through the app's `onDockUnsupported` when it has one, else the page note (TIN-756). */
|
|
90
|
+
export declare function refusalTeller(options: Pick<PinningOptions, "onDockUnsupported" | "direct" | "strings">): (message: string, drag: AgentDrag, target: FocusTarget) => void;
|
|
91
|
+
/**
|
|
92
|
+
* What an app does with a native drop of a dock agent it answered on dragover (`preventDefault`): pins it, hands an
|
|
93
|
+
* agent it does not know to `onUnknownAgent`, or says why it did nothing with `tell`. A drop the browser delivered
|
|
94
|
+
* is never left without a word (TIN-756): the extension stands back from an app that answered the dragover, so a
|
|
95
|
+
* silent refusal here was a drop with no answer at all.
|
|
96
|
+
*/
|
|
97
|
+
export declare function placeNativeDrop(options: Pick<PinningOptions, "canTarget" | "onPin" | "resolveAgent" | "onUnknownAgent" | "strings">, drag: AgentDrag | null, hit: {
|
|
98
|
+
element: Element;
|
|
99
|
+
target: FocusTarget;
|
|
100
|
+
} | null, tell: (message: string, drag: AgentDrag, target: FocusTarget) => void): "pinned" | "unknown_agent" | "refused" | "none";
|
|
77
101
|
/** What an agent's drag carries. hub and ticket come with pairing (TIN-382): the hub's origin and a single-use, short-lived ticket. */
|
|
78
102
|
export interface AgentDrag {
|
|
79
103
|
v: 1;
|
package/dist/pinning.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { APC_JSON_MAX_BYTES, APC_JSON_PATH,
|
|
1
|
+
import { APC_JSON_MAX_BYTES, APC_JSON_PATH, parseApcManifest, parseFocusAttrs, urlOnOrigin } from "@tineira/apc-protocol";
|
|
2
|
+
import { DEFAULT_PINNING_STRINGS, resolveStrings } from "./strings.js";
|
|
2
3
|
const profileLoads = new Map();
|
|
3
4
|
/** How long the page waits for its own apc.json: a held drop never waits longer. */
|
|
4
5
|
export const APC_JSON_LOAD_MS = 3000;
|
|
@@ -61,7 +62,8 @@ export function loadApcProfiles(origin = location.origin, fetcher = (...a) => fe
|
|
|
61
62
|
* replaces it: never a stack of notes.
|
|
62
63
|
*/
|
|
63
64
|
let shownNote = null;
|
|
64
|
-
export function showDockUnsupported(answer) {
|
|
65
|
+
export function showDockUnsupported(answer, given) {
|
|
66
|
+
const strings = resolveStrings(DEFAULT_PINNING_STRINGS, given);
|
|
65
67
|
if (shownNote) {
|
|
66
68
|
clearTimeout(shownNote.timer);
|
|
67
69
|
shownNote.box.remove();
|
|
@@ -73,20 +75,21 @@ export function showDockUnsupported(answer) {
|
|
|
73
75
|
Object.assign(box.style, { position: "fixed", zIndex: "2147483647", right: "16px", bottom: "calc(16px + max(var(--apc-bar-height, 0px), env(safe-area-inset-bottom, 0px)))", maxWidth: "340px", padding: "12px 14px", borderRadius: "10px", background: "#1f1f1f", color: "#fff", font: "13px/1.45 system-ui, sans-serif", boxShadow: "0 4px 16px rgba(0,0,0,.25)" });
|
|
74
76
|
const text = document.createElement("p");
|
|
75
77
|
text.textContent = answer.message;
|
|
76
|
-
Object.assign(text.style, { margin: "0 0 8px" });
|
|
77
|
-
box.appendChild(text);
|
|
78
78
|
// Only an http(s) page of this app: the same check apc.json's direct.connect passes (no blob:, javascript: or another site).
|
|
79
79
|
const connect = typeof location === "undefined" ? null : urlOnOrigin(answer.connect, location.origin);
|
|
80
|
+
// Room under the words only when a link follows them.
|
|
81
|
+
Object.assign(text.style, { margin: connect ? "0 0 8px" : "0" });
|
|
82
|
+
box.appendChild(text);
|
|
80
83
|
if (connect) {
|
|
81
84
|
const link = document.createElement("a");
|
|
82
85
|
link.setAttribute("href", connect);
|
|
83
|
-
link.textContent =
|
|
86
|
+
link.textContent = strings.connectLink(answer.name);
|
|
84
87
|
Object.assign(link.style, { color: "#f2a46b", fontWeight: "600" });
|
|
85
88
|
box.appendChild(link);
|
|
86
89
|
}
|
|
87
90
|
const close = document.createElement("button");
|
|
88
91
|
close.setAttribute("type", "button");
|
|
89
|
-
close.setAttribute("aria-label",
|
|
92
|
+
close.setAttribute("aria-label", strings.close);
|
|
90
93
|
close.textContent = "×";
|
|
91
94
|
Object.assign(close.style, { position: "absolute", top: "4px", right: "6px", background: "none", border: "0", color: "#ccc", font: "16px system-ui, sans-serif", cursor: "pointer" });
|
|
92
95
|
const remove = () => {
|
|
@@ -102,6 +105,39 @@ export function showDockUnsupported(answer) {
|
|
|
102
105
|
shownNote = { box, timer: setTimeout(remove, 20_000) };
|
|
103
106
|
return box;
|
|
104
107
|
}
|
|
108
|
+
/** What the person is told when an app answered a dock agent's drop and then cannot place it (TIN-756). */
|
|
109
|
+
export const REFUSED_NOTE = DEFAULT_PINNING_STRINGS.refused;
|
|
110
|
+
export const UNKNOWN_AGENT_NOTE = DEFAULT_PINNING_STRINGS.unknownAgent;
|
|
111
|
+
/** How a refused drop is told: through the app's `onDockUnsupported` when it has one, else the page note (TIN-756). */
|
|
112
|
+
export function refusalTeller(options) {
|
|
113
|
+
const words = resolveStrings(DEFAULT_PINNING_STRINGS, options.strings);
|
|
114
|
+
return (message, drag, target) => (options.onDockUnsupported ?? ((answer) => showDockUnsupported(answer, words)))({ drag, target, name: options.direct?.name ?? words.thisApp, connect: null, reason: "refused", message });
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* What an app does with a native drop of a dock agent it answered on dragover (`preventDefault`): pins it, hands an
|
|
118
|
+
* agent it does not know to `onUnknownAgent`, or says why it did nothing with `tell`. A drop the browser delivered
|
|
119
|
+
* is never left without a word (TIN-756): the extension stands back from an app that answered the dragover, so a
|
|
120
|
+
* silent refusal here was a drop with no answer at all.
|
|
121
|
+
*/
|
|
122
|
+
export function placeNativeDrop(options, drag, hit, tell) {
|
|
123
|
+
if (!drag || !hit)
|
|
124
|
+
return "none";
|
|
125
|
+
const grantId = options.resolveAgent ? options.resolveAgent(drag.agent) : drag.agent;
|
|
126
|
+
if (!grantId) {
|
|
127
|
+
if (drag.hub && drag.ticket && options.onUnknownAgent) {
|
|
128
|
+
options.onUnknownAgent({ ...drag, hub: drag.hub, ticket: drag.ticket }, hit.target, hit.element);
|
|
129
|
+
return "unknown_agent";
|
|
130
|
+
}
|
|
131
|
+
tell(resolveStrings(DEFAULT_PINNING_STRINGS, options.strings).unknownAgent, drag, hit.target);
|
|
132
|
+
return "refused";
|
|
133
|
+
}
|
|
134
|
+
if (options.canTarget && !options.canTarget(grantId, hit.target)) {
|
|
135
|
+
tell(resolveStrings(DEFAULT_PINNING_STRINGS, options.strings).refused, drag, hit.target);
|
|
136
|
+
return "refused";
|
|
137
|
+
}
|
|
138
|
+
options.onPin(grantId, hit.target, hit.element);
|
|
139
|
+
return "pinned";
|
|
140
|
+
}
|
|
105
141
|
/** The drag type a dock puts on an agent: its payload is agentDragPayload(handle), never a secret. */
|
|
106
142
|
export const APC_AGENT_TYPE = "application/x-apc-agent";
|
|
107
143
|
/**
|
|
@@ -202,7 +238,7 @@ body.apc-armed [data-focus] { outline: 2px dashed var(--apc-accent, #b85209); ou
|
|
|
202
238
|
[data-apc-mark][aria-pressed="true"] { outline: 3px solid var(--apc-accent, #b85209); outline-offset: 2px; }
|
|
203
239
|
`;
|
|
204
240
|
/** The note shown while an agent is armed: what to do, and a way out. Fixed above the agent bar (--apc-bar-height). */
|
|
205
|
-
function armedNote(name, onCancel) {
|
|
241
|
+
function armedNote(name, onCancel, words) {
|
|
206
242
|
const box = document.createElement("div");
|
|
207
243
|
box.setAttribute("role", "status");
|
|
208
244
|
box.setAttribute("data-apc-tap-pin", "");
|
|
@@ -212,11 +248,11 @@ function armedNote(name, onCancel) {
|
|
|
212
248
|
const say = (t) => {
|
|
213
249
|
text.textContent = t;
|
|
214
250
|
};
|
|
215
|
-
say(
|
|
251
|
+
say(words.tapPrompt(name));
|
|
216
252
|
const cancel = document.createElement("button");
|
|
217
253
|
cancel.setAttribute("type", "button");
|
|
218
254
|
cancel.setAttribute("data-apc-tap-cancel", "");
|
|
219
|
-
cancel.textContent =
|
|
255
|
+
cancel.textContent = words.cancel;
|
|
220
256
|
Object.assign(cancel.style, { minHeight: "44px", minWidth: "64px", padding: "0 14px", borderRadius: "10px", border: "1px solid #666", background: "transparent", color: "#fff", font: "600 14px system-ui, sans-serif", cursor: "pointer" });
|
|
221
257
|
cancel.addEventListener("click", onCancel);
|
|
222
258
|
box.append(text, cancel);
|
|
@@ -239,6 +275,7 @@ function armedNote(name, onCancel) {
|
|
|
239
275
|
*/
|
|
240
276
|
export function enablePinning(options) {
|
|
241
277
|
const root = options.root ?? document;
|
|
278
|
+
const words = resolveStrings(DEFAULT_PINNING_STRINGS, options.strings);
|
|
242
279
|
const markSelector = options.markSelector ?? "[data-apc-mark]";
|
|
243
280
|
let drag = null;
|
|
244
281
|
// Tap to pin: the agent armed by a tap on its mark, and the note that says so.
|
|
@@ -276,8 +313,8 @@ export function enablePinning(options) {
|
|
|
276
313
|
document.head.append(style);
|
|
277
314
|
}
|
|
278
315
|
const mark = [...document.querySelectorAll(markSelector)].find((m) => m.getAttribute("data-grant") === armed && !m.closest("apc-pin"));
|
|
279
|
-
const name = mark?.getAttribute("aria-label") || mark?.getAttribute("title") ||
|
|
280
|
-
note = armedNote(name, () => tapPin.cancel());
|
|
316
|
+
const name = mark?.getAttribute("aria-label") || mark?.getAttribute("title") || words.yourAgent;
|
|
317
|
+
note = armedNote(name, () => tapPin.cancel(), words);
|
|
281
318
|
document.body.append(note.box);
|
|
282
319
|
document.body.classList.add("apc-armed", "apc-dragging");
|
|
283
320
|
document.documentElement.setAttribute("data-apc-armed", armed);
|
|
@@ -310,7 +347,7 @@ export function enablePinning(options) {
|
|
|
310
347
|
restore.push([el, null]);
|
|
311
348
|
el.setAttribute("tabindex", "0");
|
|
312
349
|
}
|
|
313
|
-
note?.say(
|
|
350
|
+
note?.say(words.keyboardArmed);
|
|
314
351
|
targets[0]?.focus();
|
|
315
352
|
};
|
|
316
353
|
const setCurrent = (next) => {
|
|
@@ -399,7 +436,7 @@ export function enablePinning(options) {
|
|
|
399
436
|
swallowClickUntil = Date.now() + 500;
|
|
400
437
|
swallowOn = hit?.element ?? null;
|
|
401
438
|
if (result === "refused")
|
|
402
|
-
note?.say(
|
|
439
|
+
note?.say(words.refusedTap);
|
|
403
440
|
};
|
|
404
441
|
// Only the click that follows a pinning (or refused) tap is swallowed; every other click goes through.
|
|
405
442
|
const onClickCapture = (event) => {
|
|
@@ -498,7 +535,7 @@ export function enablePinning(options) {
|
|
|
498
535
|
if (fromDock && unread() && typeof given === "function")
|
|
499
536
|
load(given());
|
|
500
537
|
// A dock agent dropped before the app's profiles are known: held until they are, within the
|
|
501
|
-
// loader's own time limit (loadApcProfiles: 3 s; the ticket lives
|
|
538
|
+
// loader's own time limit (loadApcProfiles: 3 s; the ticket lives 240 s).
|
|
502
539
|
if (fromDock && loading) {
|
|
503
540
|
void ready.then(() => deliver(drag, hit));
|
|
504
541
|
return;
|
|
@@ -509,26 +546,17 @@ export function enablePinning(options) {
|
|
|
509
546
|
// A dock agent on an app that does not offer Dock, or whose profiles could not be read: answered,
|
|
510
547
|
// never sent to pairing (an app that cannot check its own file pairs nothing on a guess).
|
|
511
548
|
if (drag?.hub && drag.ticket && (dockless() || unread())) {
|
|
512
|
-
const name = options.direct?.name ??
|
|
549
|
+
const name = options.direct?.name ?? words.thisApp;
|
|
513
550
|
const reason = dockless() ? "direct_only" : "unchecked";
|
|
514
|
-
const answer = { drag, target: hit?.target ?? null, name, connect: options.direct?.connect ?? null, reason, message: reason === "direct_only" ? directOnlyNote(name) : couldNotCheckNote(name) };
|
|
515
|
-
(options.onDockUnsupported ?? showDockUnsupported)(answer);
|
|
551
|
+
const answer = { drag, target: hit?.target ?? null, name, connect: options.direct?.connect ?? null, reason, message: reason === "direct_only" ? words.directOnlyNote(name) : words.couldNotCheckNote(name) };
|
|
552
|
+
(options.onDockUnsupported ?? ((a) => showDockUnsupported(a, words)))(answer);
|
|
516
553
|
return;
|
|
517
554
|
}
|
|
518
555
|
// A drop held while the profiles loaded, on an app that turns out to offer Dock but takes no native drops.
|
|
519
556
|
if (!options.acceptNativeDrops)
|
|
520
557
|
return;
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const grantId = options.resolveAgent ? options.resolveAgent(drag.agent) : drag.agent;
|
|
524
|
-
if (!grantId) {
|
|
525
|
-
if (drag.hub && drag.ticket)
|
|
526
|
-
options.onUnknownAgent?.({ ...drag, hub: drag.hub, ticket: drag.ticket }, hit.target, hit.element);
|
|
527
|
-
return;
|
|
528
|
-
}
|
|
529
|
-
if (options.canTarget && !options.canTarget(grantId, hit.target))
|
|
530
|
-
return;
|
|
531
|
-
options.onPin(grantId, hit.target, hit.element);
|
|
558
|
+
// Through the app's own handler when it has one (Pulse and Signals show it in their own banners), else the page note.
|
|
559
|
+
placeNativeDrop(options, drag, hit, refusalTeller(options));
|
|
532
560
|
};
|
|
533
561
|
const onKey = (event) => {
|
|
534
562
|
swallowClickUntil = 0; // a new key press: nothing from before is swallowed
|
|
@@ -547,7 +575,7 @@ export function enablePinning(options) {
|
|
|
547
575
|
swallowClickUntil = Date.now() + 500;
|
|
548
576
|
swallowOn = hit.element;
|
|
549
577
|
if (tapPin.tap(hit) === "refused")
|
|
550
|
-
note?.say(
|
|
578
|
+
note?.say(words.refusedKeys);
|
|
551
579
|
}
|
|
552
580
|
};
|
|
553
581
|
root.addEventListener("pointerdown", onDown);
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/** What `<apc-agents>` says. Tooltips and `aria-label`s included. */
|
|
2
|
+
export interface BarStrings {
|
|
3
|
+
/** The bar's own `aria-label` (unless the element has one). */
|
|
4
|
+
toolbar: string;
|
|
5
|
+
/** A mark's tooltip: the agent and what to do with it. `connected` is false until it first connects. */
|
|
6
|
+
markTitle(name: string, connected: boolean): string;
|
|
7
|
+
/** A pin dot's tooltip. `read`: the agent read the pin; `here`: its target is on this screen. */
|
|
8
|
+
pinTitle(label: string, read: boolean, here: boolean): string;
|
|
9
|
+
/** A pin dot's `aria-label`. */
|
|
10
|
+
pinAria(name: string, label: string, read: boolean): string;
|
|
11
|
+
/** The × that removes a pin: its `aria-label`. */
|
|
12
|
+
unpinAria(name: string): string;
|
|
13
|
+
/** The ⊘ button's tooltip. `both`: the agent is linked, so revoking cuts both its paths. */
|
|
14
|
+
revokeTitle(name: string, both: boolean): string;
|
|
15
|
+
/** The ⊘ button's `aria-label`. */
|
|
16
|
+
revokeAria(name: string): string;
|
|
17
|
+
/** The ⊘ button's text after the first click. */
|
|
18
|
+
revokeConfirm: string;
|
|
19
|
+
/** The armed button's tooltip and `aria-label`. */
|
|
20
|
+
revokeConfirmTitle(name: string): string;
|
|
21
|
+
/** The tag of an agent that came directly: text, then tooltip. */
|
|
22
|
+
pathDirect: string;
|
|
23
|
+
pathDirectTitle: string;
|
|
24
|
+
pathDock: string;
|
|
25
|
+
pathDockTitle: string;
|
|
26
|
+
/** The tag of an agent linked across both. */
|
|
27
|
+
pathBoth: string;
|
|
28
|
+
pathBothTitle: string;
|
|
29
|
+
/** The Unlink button: text, tooltip and `aria-label`. */
|
|
30
|
+
unlink: string;
|
|
31
|
+
unlinkTitle(name: string): string;
|
|
32
|
+
unlinkAria(name: string): string;
|
|
33
|
+
/** The ‹ button that collapses the bar: tooltip and `aria-label`. */
|
|
34
|
+
collapseTitle: string;
|
|
35
|
+
collapseAria: string;
|
|
36
|
+
/** The collapsed indicator, long form (tooltip and `aria-label`): who is reading. Names in the order given. */
|
|
37
|
+
reading(names: readonly string[]): string;
|
|
38
|
+
/** The same when nobody is. */
|
|
39
|
+
nobodyReading: string;
|
|
40
|
+
/** The collapsed indicator's tooltip and `aria-label`: the summary above, then how to open the bar. */
|
|
41
|
+
showAgents(summary: string): string;
|
|
42
|
+
/** The collapsed indicator's visible text: "2 reading". Plurals are yours. */
|
|
43
|
+
readingCount(count: number): string;
|
|
44
|
+
/** …and when no agent is reading. */
|
|
45
|
+
nobodyReadingShort: string;
|
|
46
|
+
/** The dock hint's text (the link). An app can also set `dockHint = { text, url }`, or turn it off with `dockHint = null`. */
|
|
47
|
+
dockHint: string;
|
|
48
|
+
/** The hint's × button: `aria-label`. */
|
|
49
|
+
dockHintDismiss: string;
|
|
50
|
+
/** Two or more pinned targets of one linked agent as one label: "A and B". Called with the distinct labels. */
|
|
51
|
+
joinPins(labels: readonly string[]): string;
|
|
52
|
+
}
|
|
53
|
+
export declare const DEFAULT_BAR_STRINGS: BarStrings;
|
|
54
|
+
/** What `<apc-pin>` says. */
|
|
55
|
+
export interface PinStrings {
|
|
56
|
+
/** The badge's tooltip. */
|
|
57
|
+
title(name: string, seen: boolean): string;
|
|
58
|
+
/** The × button's `aria-label`. */
|
|
59
|
+
unpinAria(name: string): string;
|
|
60
|
+
/** The mark's `aria-label`. */
|
|
61
|
+
markAria(name: string, seen: boolean): string;
|
|
62
|
+
}
|
|
63
|
+
export declare const DEFAULT_PIN_STRINGS: PinStrings;
|
|
64
|
+
/** What `<apc-direct-agents>` says, and the prompt it hands the person to paste to their agent. */
|
|
65
|
+
export interface DirectStrings {
|
|
66
|
+
/** The agent name field: `aria-label` and placeholder. */
|
|
67
|
+
nameLabel: string;
|
|
68
|
+
namePlaceholder: string;
|
|
69
|
+
save: string;
|
|
70
|
+
cancel: string;
|
|
71
|
+
rename: string;
|
|
72
|
+
hideConnection: string;
|
|
73
|
+
showConnection: string;
|
|
74
|
+
/** Above the URL, and above the prompt. */
|
|
75
|
+
urlHint: string;
|
|
76
|
+
askHint: string;
|
|
77
|
+
/** The copy buttons, and what the first says for a moment after it copied. */
|
|
78
|
+
copy: string;
|
|
79
|
+
copyPrompt: string;
|
|
80
|
+
copied: string;
|
|
81
|
+
/** The add button once there is an agent, and before. */
|
|
82
|
+
addAnother: string;
|
|
83
|
+
connectAgent: string;
|
|
84
|
+
/** The app's name in the prompt until `appName` is set. */
|
|
85
|
+
thisApp: string;
|
|
86
|
+
/** One agent's status line. */
|
|
87
|
+
status(agent: {
|
|
88
|
+
connected: boolean;
|
|
89
|
+
client?: string | null;
|
|
90
|
+
linked?: boolean;
|
|
91
|
+
}): string;
|
|
92
|
+
/** What the person pastes to their own agent to connect it. */
|
|
93
|
+
prompt(url: string, appName: string): string;
|
|
94
|
+
}
|
|
95
|
+
export declare const DEFAULT_DIRECT_STRINGS: DirectStrings;
|
|
96
|
+
/** What `enablePinning` shows the person: the tap-to-pin note and the answers to a dock agent's drop. */
|
|
97
|
+
export interface PinningStrings {
|
|
98
|
+
/** The note while an agent is armed, with the agent's name (from its mark). */
|
|
99
|
+
tapPrompt(name: string): string;
|
|
100
|
+
/** The agent's name when its mark has none. */
|
|
101
|
+
yourAgent: string;
|
|
102
|
+
/** The note's Cancel button. */
|
|
103
|
+
cancel: string;
|
|
104
|
+
/** Armed from the keyboard. */
|
|
105
|
+
keyboardArmed: string;
|
|
106
|
+
/** The armed agent may not point there: by tap, and by keyboard. */
|
|
107
|
+
refusedTap: string;
|
|
108
|
+
refusedKeys: string;
|
|
109
|
+
/** A native drop the app cannot place: the agent may not point there, or is not known. */
|
|
110
|
+
refused: string;
|
|
111
|
+
unknownAgent: string;
|
|
112
|
+
/** The app's name in those answers until `direct.name` says it. */
|
|
113
|
+
thisApp: string;
|
|
114
|
+
/** The page note for a dock agent dropped on a Direct-only app: the link to the app's connect page, and its × (`aria-label`). */
|
|
115
|
+
connectLink(appName: string): string;
|
|
116
|
+
close: string;
|
|
117
|
+
/** A dock agent dropped on a Direct-only app, and when the page could not check whether it is one. Default: the protocol's notes. */
|
|
118
|
+
directOnlyNote(appName: string): string;
|
|
119
|
+
couldNotCheckNote(appName: string): string;
|
|
120
|
+
}
|
|
121
|
+
export declare const DEFAULT_PINNING_STRINGS: PinningStrings;
|
|
122
|
+
/** The defaults with the app's overrides on top. A key set to `undefined` keeps its default. */
|
|
123
|
+
export declare function resolveStrings<T extends object>(defaults: T, given?: Partial<T> | null): T;
|
|
124
|
+
/**
|
|
125
|
+
* The locale a page's dates and times are shown in: the one the app names, else the page's own
|
|
126
|
+
* (`<html lang>`), else English. Never the operating system's: a Spanish page on an English machine
|
|
127
|
+
* reads Spanish dates.
|
|
128
|
+
*/
|
|
129
|
+
export declare function pageLocale(explicit?: string | null): string;
|
|
130
|
+
/** A time or date for the person, in the page's locale (see `pageLocale`). For an app's own labels, such as a pin's "read 12:04". */
|
|
131
|
+
export declare function formatPageTime(value: Date | number | string, options?: Intl.DateTimeFormatOptions & {
|
|
132
|
+
locale?: string | null;
|
|
133
|
+
}): string;
|
package/dist/strings.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every string the package puts in the page (TIN-668), so an app in another language ships it without
|
|
3
|
+
* a fork. Each component takes one `strings` object (a `Partial` of its type below): the keys you leave
|
|
4
|
+
* out keep their English default, which is exactly the text the package always showed. A string that
|
|
5
|
+
* holds a name, a count or a label is a function, so the app decides the word order and the plural
|
|
6
|
+
* forms: nothing here is joined from English fragments.
|
|
7
|
+
*
|
|
8
|
+
* - `<apc-agents>`: `agentBar.strings = { ... }` (type `BarStrings`)
|
|
9
|
+
* - `<apc-pin>`: `pin.strings = { ... }` (type `PinStrings`)
|
|
10
|
+
* - `<apc-direct-agents>`: `list.strings = { ... }` (type `DirectStrings`)
|
|
11
|
+
* - `enablePinning({ strings: { ... } })` (type `PinningStrings`)
|
|
12
|
+
*
|
|
13
|
+
* Left as they are, on purpose: the symbols (×, ⊘, ‹, !, ✓), which carry no language; the agents' own
|
|
14
|
+
* names, labels and URLs, which the app passes in; and the page text an agent reads, which is not this
|
|
15
|
+
* package's. The note for a dock agent dropped on a Direct-only app is the protocol's `directOnlyNote`;
|
|
16
|
+
* `PinningStrings.directOnlyNote` replaces it with the app's own words.
|
|
17
|
+
*/
|
|
18
|
+
import { couldNotCheckNote, directOnlyNote } from "@tineira/apc-protocol";
|
|
19
|
+
export const DEFAULT_BAR_STRINGS = {
|
|
20
|
+
toolbar: "Agents",
|
|
21
|
+
markTitle: (name, connected) => `${name}${connected ? "" : " (not connected yet)"}. Drag onto anything to point this agent.`,
|
|
22
|
+
pinTitle: (label, read, here) => `Pinned to ${label}: ${read ? "your agent read it" : "waiting for your agent's next call"}${here ? "" : ". Click to open it."}`,
|
|
23
|
+
pinAria: (name, label, read) => `${name} pinned to ${label}${read ? ", read" : ", not read yet"}`,
|
|
24
|
+
unpinAria: (name) => `Remove ${name}'s pin`,
|
|
25
|
+
revokeTitle: (name, both) => `Revoke ${name}: it can no longer read this app${both ? ", directly or through the dock" : ""}, at once. To let it back, connect or drop it here again.`,
|
|
26
|
+
revokeAria: (name) => `Revoke ${name}`,
|
|
27
|
+
revokeConfirm: "Revoke?",
|
|
28
|
+
revokeConfirmTitle: (name) => `Click again to revoke ${name}.`,
|
|
29
|
+
pathDirect: "direct",
|
|
30
|
+
pathDirectTitle: "Connected directly, on this app's own MCP URL",
|
|
31
|
+
pathDock: "via dock",
|
|
32
|
+
pathDockTitle: "Came through an agent dock",
|
|
33
|
+
pathBoth: "direct + dock",
|
|
34
|
+
pathBothTitle: "The same agent, connected directly and through a dock. Each way keeps its own access; Revoke cuts both.",
|
|
35
|
+
unlink: "unlink",
|
|
36
|
+
unlinkTitle: (name) => `Show ${name}'s two connections as two agents again. Nothing else changes.`,
|
|
37
|
+
unlinkAria: (name) => `Unlink ${name}`,
|
|
38
|
+
collapseTitle: "Collapse to who is reading. Your agents came through your dock: drag them from there.",
|
|
39
|
+
collapseAria: "Collapse the agent bar",
|
|
40
|
+
reading: (names) => (names.length === 1 ? `${names[0]} is reading` : `${names.slice(0, -1).join(", ")} and ${names.at(-1)} are reading`),
|
|
41
|
+
nobodyReading: "No agent is reading",
|
|
42
|
+
showAgents: (summary) => `${summary}. Show the agents.`,
|
|
43
|
+
readingCount: (count) => `${count} reading`,
|
|
44
|
+
nobodyReadingShort: "no agent reading",
|
|
45
|
+
dockHint: "Use this agent across apps with the dock",
|
|
46
|
+
dockHintDismiss: "Dismiss this hint",
|
|
47
|
+
joinPins: (labels) => labels.join(" and "),
|
|
48
|
+
};
|
|
49
|
+
export const DEFAULT_PIN_STRINGS = {
|
|
50
|
+
title: (name, seen) => `${name}: ${seen ? "read this pin" : "has not read this pin yet"}. Drag to point it at something else.`,
|
|
51
|
+
unpinAria: (name) => `Remove ${name}'s pin`,
|
|
52
|
+
markAria: (name, seen) => `${name}, ${seen ? "has read its pin" : "has not read its pin yet"}`,
|
|
53
|
+
};
|
|
54
|
+
export const DEFAULT_DIRECT_STRINGS = {
|
|
55
|
+
nameLabel: "Agent name",
|
|
56
|
+
namePlaceholder: "Empty: back to its own name",
|
|
57
|
+
save: "Save",
|
|
58
|
+
cancel: "Cancel",
|
|
59
|
+
rename: "Rename",
|
|
60
|
+
hideConnection: "Hide connection",
|
|
61
|
+
showConnection: "Show connection",
|
|
62
|
+
urlHint: "Add this URL to your agent as a remote MCP server (Streamable HTTP). No login: the secret in the URL is its access, so give each agent its own.",
|
|
63
|
+
askHint: "Don't know how? Paste this to your agent:",
|
|
64
|
+
copy: "Copy",
|
|
65
|
+
copyPrompt: "Copy prompt",
|
|
66
|
+
copied: "Copied",
|
|
67
|
+
addAnother: "Add another agent",
|
|
68
|
+
connectAgent: "Connect an agent",
|
|
69
|
+
thisApp: "this app",
|
|
70
|
+
status: (a) => {
|
|
71
|
+
if (!a.connected)
|
|
72
|
+
return "not connected yet";
|
|
73
|
+
const as = a.client ? `connected as ${a.client}` : "connected";
|
|
74
|
+
return a.linked ? `${as}, also through your dock` : as;
|
|
75
|
+
},
|
|
76
|
+
prompt: (url, appName) => `Please connect this remote MCP server, named "${appName}". URL: ${url}. It uses Streamable HTTP and needs no OAuth or API key: the access is in the URL. If you can add it yourself, do it now and tell me when it's connected. If you can't, tell me the exact steps to add it in the app I'm using you in right now. Do not open the URL in a browser.`,
|
|
77
|
+
};
|
|
78
|
+
export const DEFAULT_PINNING_STRINGS = {
|
|
79
|
+
tapPrompt: (name) => `Tap what to point ${name} at.`,
|
|
80
|
+
yourAgent: "your agent",
|
|
81
|
+
cancel: "Cancel",
|
|
82
|
+
keyboardArmed: "Pointing your agent: Tab to an outlined item, then press Enter or Space to pin it. Escape cancels.",
|
|
83
|
+
refusedTap: "This agent cannot point at that. Tap another outlined target, or Cancel.",
|
|
84
|
+
refusedKeys: "This agent cannot point at that. Tab to another outlined item, or press Escape.",
|
|
85
|
+
refused: "This agent cannot point at that. Drop it on another highlighted item.",
|
|
86
|
+
unknownAgent: "This app does not know that agent.",
|
|
87
|
+
thisApp: "This app",
|
|
88
|
+
connectLink: (appName) => `Connect an agent to ${appName}`,
|
|
89
|
+
close: "Close",
|
|
90
|
+
directOnlyNote,
|
|
91
|
+
couldNotCheckNote,
|
|
92
|
+
};
|
|
93
|
+
/** The defaults with the app's overrides on top. A key set to `undefined` keeps its default. */
|
|
94
|
+
export function resolveStrings(defaults, given) {
|
|
95
|
+
const out = { ...defaults };
|
|
96
|
+
if (given)
|
|
97
|
+
for (const key of Object.keys(given))
|
|
98
|
+
if (given[key] !== undefined)
|
|
99
|
+
out[key] = given[key];
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The locale a page's dates and times are shown in: the one the app names, else the page's own
|
|
104
|
+
* (`<html lang>`), else English. Never the operating system's: a Spanish page on an English machine
|
|
105
|
+
* reads Spanish dates.
|
|
106
|
+
*/
|
|
107
|
+
export function pageLocale(explicit) {
|
|
108
|
+
const lang = explicit || (typeof document === "undefined" ? "" : document.documentElement?.lang);
|
|
109
|
+
const tag = (lang ?? "").trim();
|
|
110
|
+
if (!tag)
|
|
111
|
+
return "en";
|
|
112
|
+
try {
|
|
113
|
+
return Intl.getCanonicalLocales(tag)[0] ?? "en";
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return "en";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** A time or date for the person, in the page's locale (see `pageLocale`). For an app's own labels, such as a pin's "read 12:04". */
|
|
120
|
+
export function formatPageTime(value, options) {
|
|
121
|
+
const { locale, ...given } = options ?? {};
|
|
122
|
+
// No format option at all: a date and a time, in the medium and short styles.
|
|
123
|
+
const format = Object.keys(given).length ? given : { dateStyle: "medium", timeStyle: "short" };
|
|
124
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
125
|
+
return new Intl.DateTimeFormat(pageLocale(locale), format).format(date);
|
|
126
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tineira/apc-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Agent Pointed Context (APC) for an app's page, in the browser: pinning agents to what the person sees, the agent bar (<apc-agents>), the pin badge, presence and the Direct agents list.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"apc",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@tineira/apc-protocol": "^0.
|
|
43
|
+
"@tineira/apc-protocol": "^0.9.0"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {},
|
|
46
46
|
"types": "./dist/index.d.ts"
|