@frockbot/plugin-flock 0.3.10 → 0.3.12
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/package.json +8 -8
- package/src/client/BotAvatar.vue +118 -4
- package/src/client/FlockSidebar.vue +106 -6
- package/src/client/SheepAvatar.vue +1 -1
- package/src/client/delivered-notifications.test.ts +103 -0
- package/src/client/delivered-notifications.ts +100 -0
- package/src/client/index.test.ts +97 -2
- package/src/client/index.ts +236 -52
- package/src/client/sidebar.test.ts +47 -1
- package/src/client/sidebar.ts +37 -0
- package/src/client/state.ts +4 -2
- package/src/client/styles.css +107 -0
- package/src/shared.test.ts +22 -0
- package/src/shared.ts +18 -1
package/src/client/state.ts
CHANGED
|
@@ -24,8 +24,10 @@ export interface FlockWebData {
|
|
|
24
24
|
* Whether a directory read has ever completed.
|
|
25
25
|
*
|
|
26
26
|
* "No Bots yet." is a fact about the User's account, and it can only be
|
|
27
|
-
* stated once the account has been read. Before that — the first paint,
|
|
28
|
-
*
|
|
27
|
+
* stated once the account has been read. Before that — the first paint, the
|
|
28
|
+
* reload after a Bot is created, and every read that failed — the list is
|
|
29
|
+
* unknown, not empty, and offering to create a first Bot to someone who
|
|
30
|
+
* already has several is the worst thing this column can say.
|
|
29
31
|
*/
|
|
30
32
|
loaded: boolean;
|
|
31
33
|
error?: string;
|
package/src/client/styles.css
CHANGED
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
border-radius: 9px;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/* The pinned sidebar tile: round, so a pin reads as a destination not a row. */
|
|
20
|
+
.flock-avatar--tile {
|
|
21
|
+
width: var(--frock-avatar-lg);
|
|
22
|
+
height: var(--frock-avatar-lg);
|
|
23
|
+
border-radius: 50%;
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
.flock-avatar--large {
|
|
20
27
|
width: 188px;
|
|
21
28
|
height: 188px;
|
|
@@ -63,6 +70,88 @@
|
|
|
63
70
|
color: var(--frock-text);
|
|
64
71
|
}
|
|
65
72
|
|
|
73
|
+
/*
|
|
74
|
+
* Pinned Bots. A wrapping row of large round tiles above the labelled groups,
|
|
75
|
+
* each one a whole Bot rather than a line of text about it.
|
|
76
|
+
*/
|
|
77
|
+
.flock-pinned {
|
|
78
|
+
display: flex;
|
|
79
|
+
flex-wrap: wrap;
|
|
80
|
+
gap: 6px;
|
|
81
|
+
padding: 4px 6px 10px;
|
|
82
|
+
border-bottom: 1px solid var(--frock-border);
|
|
83
|
+
margin-bottom: 8px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.flock-pinned-tile {
|
|
87
|
+
display: flex;
|
|
88
|
+
width: 72px;
|
|
89
|
+
flex: 0 0 auto;
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
align-items: center;
|
|
92
|
+
gap: 6px;
|
|
93
|
+
padding: 6px 2px;
|
|
94
|
+
border: 0;
|
|
95
|
+
border-radius: var(--frock-radius-card);
|
|
96
|
+
background: transparent;
|
|
97
|
+
color: inherit;
|
|
98
|
+
font: inherit;
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
transition: background-color var(--frock-motion-fast);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.flock-pinned-tile:hover:not(:disabled) {
|
|
104
|
+
background: var(--frock-fill-hover);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.flock-pinned-tile.active {
|
|
108
|
+
background: var(--frock-fill-pressed);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.flock-pinned-tile.archived {
|
|
112
|
+
opacity: 0.72;
|
|
113
|
+
cursor: default;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.flock-pinned-tile:focus-visible {
|
|
117
|
+
outline: 2px solid var(--frock-focus-ring);
|
|
118
|
+
outline-offset: 2px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.flock-pinned-art {
|
|
122
|
+
position: relative;
|
|
123
|
+
display: inline-flex;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* The tile has no preview line, so unread is a dot rather than bold text. */
|
|
127
|
+
.flock-pinned-dot {
|
|
128
|
+
position: absolute;
|
|
129
|
+
right: 0;
|
|
130
|
+
bottom: 2px;
|
|
131
|
+
width: 10px;
|
|
132
|
+
height: 10px;
|
|
133
|
+
border-radius: 999px;
|
|
134
|
+
background: var(--frock-action-primary);
|
|
135
|
+
box-shadow: 0 0 0 2px var(--frock-surface);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.flock-pinned-name {
|
|
139
|
+
max-width: 100%;
|
|
140
|
+
overflow: hidden;
|
|
141
|
+
color: var(--frock-text-muted);
|
|
142
|
+
font-size: var(--frock-text-xs);
|
|
143
|
+
line-height: var(--frock-leading-tight);
|
|
144
|
+
text-align: center;
|
|
145
|
+
text-overflow: ellipsis;
|
|
146
|
+
white-space: nowrap;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.flock-pinned-tile.active .flock-pinned-name,
|
|
150
|
+
.flock-pinned-tile.unread .flock-pinned-name {
|
|
151
|
+
color: var(--frock-text);
|
|
152
|
+
font-weight: 600;
|
|
153
|
+
}
|
|
154
|
+
|
|
66
155
|
.flock-groups,
|
|
67
156
|
.flock-group,
|
|
68
157
|
.flock-rows {
|
|
@@ -268,6 +357,24 @@
|
|
|
268
357
|
color: var(--frock-danger-text);
|
|
269
358
|
}
|
|
270
359
|
|
|
360
|
+
/* The way to try the read again, beside the sentence that says it failed. */
|
|
361
|
+
.flock-retry {
|
|
362
|
+
display: inline-block;
|
|
363
|
+
margin-top: 6px;
|
|
364
|
+
border: 1px solid var(--frock-border);
|
|
365
|
+
border-radius: var(--frock-radius-control);
|
|
366
|
+
background: transparent;
|
|
367
|
+
color: var(--frock-text);
|
|
368
|
+
padding: 2px 8px;
|
|
369
|
+
font: inherit;
|
|
370
|
+
font-size: var(--frock-text-xs);
|
|
371
|
+
cursor: pointer;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.flock-retry:hover {
|
|
375
|
+
background: var(--frock-fill-hover);
|
|
376
|
+
}
|
|
377
|
+
|
|
271
378
|
/* Header identity */
|
|
272
379
|
|
|
273
380
|
.flock-identity-button {
|
package/src/shared.test.ts
CHANGED
|
@@ -184,6 +184,28 @@ describe("Flock v1 contracts", () => {
|
|
|
184
184
|
}),
|
|
185
185
|
).toThrow("duplicate IDs");
|
|
186
186
|
});
|
|
187
|
+
|
|
188
|
+
test("carries the pin instant, and refuses one that is not an instant", () => {
|
|
189
|
+
const identity = {
|
|
190
|
+
schemaVersion: 1 as const,
|
|
191
|
+
botId: "alpha",
|
|
192
|
+
name: "Atlas",
|
|
193
|
+
namedBy: "user" as const,
|
|
194
|
+
hiddenFromSidebar: false,
|
|
195
|
+
pinnedAt: "2026-09-03T10:15:00.000Z",
|
|
196
|
+
};
|
|
197
|
+
expect(decodeBotIdentityViewV1(structuredClone(identity))).toEqual(
|
|
198
|
+
identity,
|
|
199
|
+
);
|
|
200
|
+
// Absent stays absent: every identity written before pinning existed.
|
|
201
|
+
const { pinnedAt: _pinnedAt, ...unpinned } = identity;
|
|
202
|
+
expect(decodeBotIdentityViewV1(structuredClone(unpinned))).toEqual(
|
|
203
|
+
unpinned,
|
|
204
|
+
);
|
|
205
|
+
expect(() =>
|
|
206
|
+
decodeBotIdentityViewV1({ ...identity, pinnedAt: "someday" }),
|
|
207
|
+
).toThrow("pinnedAt is invalid");
|
|
208
|
+
});
|
|
187
209
|
});
|
|
188
210
|
|
|
189
211
|
describe("stored Bot directory migration", () => {
|
package/src/shared.ts
CHANGED
|
@@ -137,6 +137,12 @@ export interface BotIdentityViewV1 {
|
|
|
137
137
|
/** Purely organisational sidebar group; never part of Bot instructions. */
|
|
138
138
|
label?: string;
|
|
139
139
|
title?: string;
|
|
140
|
+
/**
|
|
141
|
+
* When the User pinned this Bot, as an ISO 8601 instant. A pinned Bot is
|
|
142
|
+
* shown as a tile above the list instead of a row inside it, earliest pin
|
|
143
|
+
* first. Absent means not pinned.
|
|
144
|
+
*/
|
|
145
|
+
pinnedAt?: string;
|
|
140
146
|
}
|
|
141
147
|
|
|
142
148
|
export interface BotIdentityDirectoryViewV1 {
|
|
@@ -600,7 +606,7 @@ export function decodeBotIdentityViewV1(input: unknown): BotIdentityViewV1 {
|
|
|
600
606
|
exact(
|
|
601
607
|
value,
|
|
602
608
|
["schemaVersion", "botId", "name", "namedBy", "hiddenFromSidebar"],
|
|
603
|
-
["label", "title"],
|
|
609
|
+
["label", "title", "pinnedAt"],
|
|
604
610
|
);
|
|
605
611
|
if (value.schemaVersion !== 1 || typeof value.hiddenFromSidebar !== "boolean")
|
|
606
612
|
throw new FlockDecodeError("Bot identity is invalid");
|
|
@@ -616,9 +622,20 @@ export function decodeBotIdentityViewV1(input: unknown): BotIdentityViewV1 {
|
|
|
616
622
|
...(value.title === undefined
|
|
617
623
|
? {}
|
|
618
624
|
: { title: boundedText(value.title, "title", 120) }),
|
|
625
|
+
...(value.pinnedAt === undefined
|
|
626
|
+
? {}
|
|
627
|
+
: { pinnedAt: timestampText(value.pinnedAt, "pinnedAt") }),
|
|
619
628
|
};
|
|
620
629
|
}
|
|
621
630
|
|
|
631
|
+
/** An ISO 8601 instant. The sidebar orders pinned Bots by it. */
|
|
632
|
+
function timestampText(value: unknown, label: string): string {
|
|
633
|
+
const candidate = boundedText(value, label, 64);
|
|
634
|
+
if (!Number.isFinite(Date.parse(candidate)))
|
|
635
|
+
throw new FlockDecodeError(`${label} is invalid`);
|
|
636
|
+
return candidate;
|
|
637
|
+
}
|
|
638
|
+
|
|
622
639
|
export function decodeBotIdentityDirectoryViewV1(
|
|
623
640
|
input: unknown,
|
|
624
641
|
): BotIdentityDirectoryViewV1 {
|