@frockbot/plugin-flock 0.0.0 → 0.1.1
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 +8 -1
- package/assets/amber-glasses.webp +0 -0
- package/assets/background-bright-orange.webp +0 -0
- package/assets/background-canary-yellow.webp +0 -0
- package/assets/background-electric-blue.webp +0 -0
- package/assets/background-hot-pink.webp +0 -0
- package/assets/background-lime-green.webp +0 -0
- package/assets/background-vivid-purple.webp +0 -0
- package/assets/blue-sailor-cap.webp +0 -0
- package/assets/bronze-aviator-goggles.webp +0 -0
- package/assets/burgundy-beret.webp +0 -0
- package/assets/canary-party-hat.webp +0 -0
- package/assets/canary-rain-cap.webp +0 -0
- package/assets/canonical.webp +0 -0
- package/assets/chef-toque.webp +0 -0
- package/assets/cobalt-round-glasses.webp +0 -0
- package/assets/coral-bucket-hat.webp +0 -0
- package/assets/emerald-necktie.webp +0 -0
- package/assets/forest-ranger-fedora.webp +0 -0
- package/assets/gold-royal-crown.webp +0 -0
- package/assets/gold-sun-medallion.webp +0 -0
- package/assets/graduation-cap.webp +0 -0
- package/assets/green-square-glasses.webp +0 -0
- package/assets/layers.css +159 -0
- package/assets/leather-bell.webp +0 -0
- package/assets/lilac-flower-crown.webp +0 -0
- package/assets/manifest.json +769 -0
- package/assets/matte-black-top-hat.webp +0 -0
- package/assets/navy-wizard-hat.webp +0 -0
- package/assets/orange-hard-hat.webp +0 -0
- package/assets/plum-glasses.webp +0 -0
- package/assets/purple-scarf.webp +0 -0
- package/assets/purple-witch-hat.webp +0 -0
- package/assets/rainbow-propeller-cap.webp +0 -0
- package/assets/rainbow-visor.webp +0 -0
- package/assets/ranger-fedora-feather.webp +0 -0
- package/assets/red-polka-bowtie.webp +0 -0
- package/assets/retro-3d-glasses.webp +0 -0
- package/assets/rose-heart-sunglasses.webp +0 -0
- package/assets/royal-crown-rubies.webp +0 -0
- package/assets/sailor-cap-anchor.webp +0 -0
- package/assets/silver-space-helmet.webp +0 -0
- package/assets/straw-boater.webp +0 -0
- package/assets/teal-bowtie.webp +0 -0
- package/assets/teal-glasses.webp +0 -0
- package/assets/terracotta-beanie.webp +0 -0
- package/assets/tortoiseshell-readers.webp +0 -0
- package/assets/turquoise-bandana.webp +0 -0
- package/assets/violet-masquerade-mask.webp +0 -0
- package/assets/white-snow-goggles.webp +0 -0
- package/assets/white-tennis-visor.webp +0 -0
- package/assets/witch-hat-moon.webp +0 -0
- package/assets/yellow-star-glasses.webp +0 -0
- package/frockbot.json +38 -0
- package/package.json +47 -6
- package/src/agent.test.ts +401 -0
- package/src/agent.ts +571 -0
- package/src/assets.test.ts +31 -0
- package/src/backend.test.ts +205 -0
- package/src/backend.ts +283 -0
- package/src/bot.test.ts +190 -0
- package/src/bot.ts +255 -0
- package/src/client/BotAvatar.vue +20 -0
- package/src/client/FlockAvatar.vue +33 -0
- package/src/client/FlockAvatarEditor.vue +44 -0
- package/src/client/FlockCreateButton.vue +18 -0
- package/src/client/FlockIdentity.vue +31 -0
- package/src/client/FlockOverlay.vue +209 -0
- package/src/client/FlockSidebar.vue +254 -0
- package/src/client/SheepAvatar.vue +28 -0
- package/src/client/dialog-focus.test.ts +15 -0
- package/src/client/dialog-focus.ts +13 -0
- package/src/client/index.test.ts +502 -0
- package/src/client/index.ts +619 -0
- package/src/client/mobile-dialog-layout.test.ts +50 -0
- package/src/client/pending-create.test.ts +71 -0
- package/src/client/pending-create.ts +93 -0
- package/src/client/sidebar.test.ts +77 -0
- package/src/client/sidebar.ts +77 -0
- package/src/client/state.ts +53 -0
- package/src/client/styles.css +611 -0
- package/src/env.d.ts +9 -0
- package/src/index.ts +4 -0
- package/src/manifest.ts +3 -0
- package/src/package.test.ts +12 -0
- package/src/shared.test.ts +197 -0
- package/src/shared.ts +735 -0
- package/src/user.test.ts +455 -0
- package/src/user.ts +465 -0
- package/tsconfig.json +15 -0
- package/validate_readability.py +64 -0
- package/vite.config.ts +30 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { UiSkeleton } from "@frockbot/client-ui";
|
|
3
|
+
import { computed, inject, onMounted } from "vue";
|
|
4
|
+
import { frockBotWebDataKey } from "@frockbot/plugin-shell/shared";
|
|
5
|
+
import { flockWebDataKey } from "./state.js";
|
|
6
|
+
import { formatSidebarMessageTimeV1, groupSidebarBotsV1 } from "./sidebar.js";
|
|
7
|
+
import BotAvatar from "./BotAvatar.vue";
|
|
8
|
+
|
|
9
|
+
const injectedFlock = inject(flockWebDataKey);
|
|
10
|
+
const injectedShell = inject(frockBotWebDataKey);
|
|
11
|
+
if (!injectedFlock || !injectedShell)
|
|
12
|
+
throw new Error("Flock client data was not provided");
|
|
13
|
+
const flock = injectedFlock;
|
|
14
|
+
const shell = injectedShell;
|
|
15
|
+
flock.value.bindShell(shell);
|
|
16
|
+
const active = computed(() => shell.value.activeBotId);
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* Hidden and archived are different states. Archiving stops a Bot working;
|
|
20
|
+
* hiding only takes it out of this list, so a hidden Bot stays selectable and
|
|
21
|
+
* the "Hidden" group is how a User reaches it again.
|
|
22
|
+
*/
|
|
23
|
+
function isHidden(botId: string): boolean {
|
|
24
|
+
return flock.value.profiles[botId]?.hiddenFromSidebar === true;
|
|
25
|
+
}
|
|
26
|
+
const listedBots = computed(() =>
|
|
27
|
+
flock.value.directory.bots.filter(
|
|
28
|
+
(bot) =>
|
|
29
|
+
flock.value.showArchived ||
|
|
30
|
+
flock.value.lifecycles[bot.botId] !== "archived",
|
|
31
|
+
),
|
|
32
|
+
);
|
|
33
|
+
const visibleBots = computed(() =>
|
|
34
|
+
listedBots.value.filter((bot) => !isHidden(bot.botId)),
|
|
35
|
+
);
|
|
36
|
+
const groupedVisibleBots = computed(() =>
|
|
37
|
+
groupSidebarBotsV1(visibleBots.value, flock.value.profiles),
|
|
38
|
+
);
|
|
39
|
+
const hiddenBots = computed(() =>
|
|
40
|
+
listedBots.value.filter((bot) => isHidden(bot.botId)),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
/** The live name the Bot's own settings hold, not the registration seed. */
|
|
44
|
+
function botName(botId: string, fallback: string): string {
|
|
45
|
+
return flock.value.profiles[botId]?.name ?? fallback;
|
|
46
|
+
}
|
|
47
|
+
function botSubtitle(botId: string): string {
|
|
48
|
+
return flock.value.profiles[botId]?.title ?? botId;
|
|
49
|
+
}
|
|
50
|
+
function previewText(botId: string): string {
|
|
51
|
+
return flock.value.unread[botId]?.lastMessage?.text ?? botSubtitle(botId);
|
|
52
|
+
}
|
|
53
|
+
function previewAt(botId: string): string | undefined {
|
|
54
|
+
return flock.value.unread[botId]?.lastMessage?.at;
|
|
55
|
+
}
|
|
56
|
+
function previewTime(botId: string): string {
|
|
57
|
+
const at = previewAt(botId);
|
|
58
|
+
return at ? formatSidebarMessageTimeV1(at) : "";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/*
|
|
62
|
+
* Unread is backend state: the Bot Durable Object derives the count and the
|
|
63
|
+
* gateway fans it out. Nothing here computes one — these read the projection.
|
|
64
|
+
*/
|
|
65
|
+
function isUnread(botId: string): boolean {
|
|
66
|
+
return flock.value.unread[botId]?.unread === true;
|
|
67
|
+
}
|
|
68
|
+
function unreadLabel(botId: string): string | undefined {
|
|
69
|
+
const view = flock.value.unread[botId];
|
|
70
|
+
if (!view?.unread || view.count === 0) return undefined;
|
|
71
|
+
return view.capped ? `${view.count}+` : String(view.count);
|
|
72
|
+
}
|
|
73
|
+
/** Hidden Bots contribute no row, so their unread arrives as one badge. */
|
|
74
|
+
const hiddenUnreadCount = computed(() =>
|
|
75
|
+
hiddenBots.value.reduce(
|
|
76
|
+
(total, bot) => total + (flock.value.unread[bot.botId]?.count ?? 0),
|
|
77
|
+
0,
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
const hiddenUnread = computed(() =>
|
|
81
|
+
hiddenBots.value.some((bot) => isUnread(bot.botId)),
|
|
82
|
+
);
|
|
83
|
+
onMounted(() => void flock.value.load());
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<template>
|
|
87
|
+
<div class="flock-list-actions">
|
|
88
|
+
<button type="button" class="flock-manage" @click="flock.toggleArchived">
|
|
89
|
+
{{ flock.showArchived ? "Hide archived" : "Manage" }}
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
<div v-if="flock.loading" class="flock-skeleton" aria-busy="true">
|
|
93
|
+
<span class="flock-skeleton-label">Loading your flock…</span>
|
|
94
|
+
<div v-for="row in 3" :key="row" class="flock-skeleton-row">
|
|
95
|
+
<UiSkeleton shape="circle" />
|
|
96
|
+
<span class="flock-skeleton-copy">
|
|
97
|
+
<UiSkeleton :width="row === 2 ? '48%' : '62%'" />
|
|
98
|
+
<UiSkeleton width="36%" />
|
|
99
|
+
</span>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
<p v-else-if="!flock.directory.bots.length" class="flock-empty">
|
|
103
|
+
No Bots yet. Add your first sheep.
|
|
104
|
+
</p>
|
|
105
|
+
<template v-else>
|
|
106
|
+
<div class="flock-groups">
|
|
107
|
+
<section
|
|
108
|
+
v-for="group in groupedVisibleBots.groups"
|
|
109
|
+
:key="group.key"
|
|
110
|
+
class="flock-group"
|
|
111
|
+
>
|
|
112
|
+
<h2 v-if="groupedVisibleBots.showHeadings" class="flock-group-heading">
|
|
113
|
+
{{ group.label }}
|
|
114
|
+
</h2>
|
|
115
|
+
<TransitionGroup name="flock-row" tag="div" class="flock-rows">
|
|
116
|
+
<div
|
|
117
|
+
v-for="bot in group.bots"
|
|
118
|
+
:key="bot.botId"
|
|
119
|
+
class="flock-bot-row"
|
|
120
|
+
:class="{
|
|
121
|
+
active: active === bot.botId,
|
|
122
|
+
archived: flock.lifecycles[bot.botId] === 'archived',
|
|
123
|
+
unread: isUnread(bot.botId),
|
|
124
|
+
}"
|
|
125
|
+
>
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
class="flock-bot-select"
|
|
129
|
+
:disabled="flock.lifecycles[bot.botId] === 'archived'"
|
|
130
|
+
:aria-current="active === bot.botId ? 'true' : undefined"
|
|
131
|
+
@click="flock.select(bot.botId)"
|
|
132
|
+
>
|
|
133
|
+
<BotAvatar
|
|
134
|
+
:bot-id="bot.botId"
|
|
135
|
+
:sheep="flock.identities[bot.botId]?.sheep ?? bot.sheep"
|
|
136
|
+
:label="`${botName(bot.botId, bot.initialName)} avatar`"
|
|
137
|
+
/>
|
|
138
|
+
<span class="flock-bot-copy">
|
|
139
|
+
<span class="flock-bot-primary">
|
|
140
|
+
<strong>{{ botName(bot.botId, bot.initialName) }}</strong>
|
|
141
|
+
<time
|
|
142
|
+
v-if="previewAt(bot.botId)"
|
|
143
|
+
:datetime="previewAt(bot.botId)"
|
|
144
|
+
>
|
|
145
|
+
{{ previewTime(bot.botId) }}
|
|
146
|
+
</time>
|
|
147
|
+
</span>
|
|
148
|
+
<small>{{ previewText(bot.botId) }}</small>
|
|
149
|
+
</span>
|
|
150
|
+
<span
|
|
151
|
+
v-if="unreadLabel(bot.botId) || active === bot.botId"
|
|
152
|
+
class="flock-bot-indicators"
|
|
153
|
+
>
|
|
154
|
+
<span
|
|
155
|
+
v-if="unreadLabel(bot.botId)"
|
|
156
|
+
class="flock-unread-badge"
|
|
157
|
+
:aria-label="`${unreadLabel(bot.botId)} unread`"
|
|
158
|
+
>{{ unreadLabel(bot.botId) }}</span
|
|
159
|
+
>
|
|
160
|
+
<i
|
|
161
|
+
v-if="active === bot.botId"
|
|
162
|
+
class="flock-bot-dot"
|
|
163
|
+
aria-hidden="true"
|
|
164
|
+
/>
|
|
165
|
+
</span>
|
|
166
|
+
</button>
|
|
167
|
+
<button
|
|
168
|
+
v-if="
|
|
169
|
+
flock.showArchived && flock.lifecycles[bot.botId] === 'archived'
|
|
170
|
+
"
|
|
171
|
+
type="button"
|
|
172
|
+
class="flock-lifecycle"
|
|
173
|
+
@click="flock.restore(bot.botId)"
|
|
174
|
+
>
|
|
175
|
+
Restore
|
|
176
|
+
</button>
|
|
177
|
+
<button
|
|
178
|
+
v-else-if="flock.showArchived"
|
|
179
|
+
type="button"
|
|
180
|
+
class="flock-lifecycle"
|
|
181
|
+
@click="flock.openArchive(bot.botId)"
|
|
182
|
+
>
|
|
183
|
+
Archive
|
|
184
|
+
</button>
|
|
185
|
+
</div>
|
|
186
|
+
</TransitionGroup>
|
|
187
|
+
</section>
|
|
188
|
+
</div>
|
|
189
|
+
<div v-if="hiddenBots.length" class="flock-hidden-group">
|
|
190
|
+
<button
|
|
191
|
+
type="button"
|
|
192
|
+
class="flock-manage flock-hidden-toggle"
|
|
193
|
+
:aria-expanded="flock.showHidden ? 'true' : 'false'"
|
|
194
|
+
@click="flock.toggleHidden"
|
|
195
|
+
>
|
|
196
|
+
{{
|
|
197
|
+
flock.showHidden
|
|
198
|
+
? `Hide ${hiddenBots.length} hidden`
|
|
199
|
+
: `Show ${hiddenBots.length} hidden`
|
|
200
|
+
}}
|
|
201
|
+
<span
|
|
202
|
+
v-if="hiddenUnread"
|
|
203
|
+
class="flock-unread-badge"
|
|
204
|
+
:aria-label="`${hiddenUnreadCount} unread in hidden Bots`"
|
|
205
|
+
>{{ hiddenUnreadCount > 0 ? hiddenUnreadCount : "" }}</span
|
|
206
|
+
>
|
|
207
|
+
</button>
|
|
208
|
+
<TransitionGroup
|
|
209
|
+
v-if="flock.showHidden"
|
|
210
|
+
name="flock-row"
|
|
211
|
+
tag="div"
|
|
212
|
+
class="flock-rows"
|
|
213
|
+
>
|
|
214
|
+
<div
|
|
215
|
+
v-for="bot in hiddenBots"
|
|
216
|
+
:key="bot.botId"
|
|
217
|
+
class="flock-bot-row"
|
|
218
|
+
:class="{
|
|
219
|
+
active: active === bot.botId,
|
|
220
|
+
archived: flock.lifecycles[bot.botId] === 'archived',
|
|
221
|
+
unread: isUnread(bot.botId),
|
|
222
|
+
}"
|
|
223
|
+
>
|
|
224
|
+
<button
|
|
225
|
+
type="button"
|
|
226
|
+
class="flock-bot-select"
|
|
227
|
+
:disabled="flock.lifecycles[bot.botId] === 'archived'"
|
|
228
|
+
:aria-current="active === bot.botId ? 'true' : undefined"
|
|
229
|
+
@click="flock.select(bot.botId)"
|
|
230
|
+
>
|
|
231
|
+
<BotAvatar
|
|
232
|
+
:bot-id="bot.botId"
|
|
233
|
+
:sheep="flock.identities[bot.botId]?.sheep ?? bot.sheep"
|
|
234
|
+
:label="`${botName(bot.botId, bot.initialName)} avatar`"
|
|
235
|
+
/>
|
|
236
|
+
<span class="flock-bot-copy">
|
|
237
|
+
<span class="flock-bot-primary">
|
|
238
|
+
<strong>{{ botName(bot.botId, bot.initialName) }}</strong>
|
|
239
|
+
<time
|
|
240
|
+
v-if="previewAt(bot.botId)"
|
|
241
|
+
:datetime="previewAt(bot.botId)"
|
|
242
|
+
>
|
|
243
|
+
{{ previewTime(bot.botId) }}
|
|
244
|
+
</time>
|
|
245
|
+
</span>
|
|
246
|
+
<small>{{ previewText(bot.botId) }}</small>
|
|
247
|
+
</span>
|
|
248
|
+
</button>
|
|
249
|
+
</div>
|
|
250
|
+
</TransitionGroup>
|
|
251
|
+
</div>
|
|
252
|
+
</template>
|
|
253
|
+
<p v-if="flock.error" class="flock-error" role="alert">{{ flock.error }}</p>
|
|
254
|
+
</template>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
import { sheepLayerIds, type SheepRecipeV1 } from "../shared.js";
|
|
4
|
+
const props = withDefaults(
|
|
5
|
+
defineProps<{
|
|
6
|
+
sheep: SheepRecipeV1;
|
|
7
|
+
label?: string;
|
|
8
|
+
size?: "mini" | "small" | "large";
|
|
9
|
+
}>(),
|
|
10
|
+
{ label: "Bot sheep", size: "small" },
|
|
11
|
+
);
|
|
12
|
+
const layers = computed(() => sheepLayerIds(props.sheep));
|
|
13
|
+
</script>
|
|
14
|
+
<template>
|
|
15
|
+
<span
|
|
16
|
+
class="flock-avatar"
|
|
17
|
+
:class="`flock-avatar--${size}`"
|
|
18
|
+
role="img"
|
|
19
|
+
:aria-label="label"
|
|
20
|
+
>
|
|
21
|
+
<i
|
|
22
|
+
v-for="layer in layers"
|
|
23
|
+
:key="layer"
|
|
24
|
+
class="flock-art"
|
|
25
|
+
:class="`flock-art--${layer}`"
|
|
26
|
+
/>
|
|
27
|
+
</span>
|
|
28
|
+
</template>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { dialogFocusWrapTarget } from "./dialog-focus.js";
|
|
3
|
+
|
|
4
|
+
describe("Flock dialog focus trap", () => {
|
|
5
|
+
test("wraps forward and reverse Tab only at dialog boundaries", () => {
|
|
6
|
+
const first = { id: "first" };
|
|
7
|
+
const middle = { id: "middle" };
|
|
8
|
+
const last = { id: "last" };
|
|
9
|
+
const controls = [first, middle, last];
|
|
10
|
+
expect(dialogFocusWrapTarget(controls, last, false)).toBe(first);
|
|
11
|
+
expect(dialogFocusWrapTarget(controls, first, true)).toBe(last);
|
|
12
|
+
expect(dialogFocusWrapTarget(controls, middle, false)).toBeUndefined();
|
|
13
|
+
expect(dialogFocusWrapTarget([], null, false)).toBeUndefined();
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Return the wrapped focus target, or undefined when Tab stays within the dialog. */
|
|
2
|
+
export function dialogFocusWrapTarget<T>(
|
|
3
|
+
controls: readonly T[],
|
|
4
|
+
active: T | null,
|
|
5
|
+
reverse: boolean,
|
|
6
|
+
): T | undefined {
|
|
7
|
+
const first = controls[0];
|
|
8
|
+
const last = controls.at(-1);
|
|
9
|
+
if (!first || !last) return undefined;
|
|
10
|
+
if (reverse && active === first) return last;
|
|
11
|
+
if (!reverse && active === last) return first;
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|