@frockbot/plugin-flock 0.0.0 → 0.1.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.
Files changed (92) hide show
  1. package/README.md +8 -1
  2. package/assets/amber-glasses.webp +0 -0
  3. package/assets/background-bright-orange.webp +0 -0
  4. package/assets/background-canary-yellow.webp +0 -0
  5. package/assets/background-electric-blue.webp +0 -0
  6. package/assets/background-hot-pink.webp +0 -0
  7. package/assets/background-lime-green.webp +0 -0
  8. package/assets/background-vivid-purple.webp +0 -0
  9. package/assets/blue-sailor-cap.webp +0 -0
  10. package/assets/bronze-aviator-goggles.webp +0 -0
  11. package/assets/burgundy-beret.webp +0 -0
  12. package/assets/canary-party-hat.webp +0 -0
  13. package/assets/canary-rain-cap.webp +0 -0
  14. package/assets/canonical.webp +0 -0
  15. package/assets/chef-toque.webp +0 -0
  16. package/assets/cobalt-round-glasses.webp +0 -0
  17. package/assets/coral-bucket-hat.webp +0 -0
  18. package/assets/emerald-necktie.webp +0 -0
  19. package/assets/forest-ranger-fedora.webp +0 -0
  20. package/assets/gold-royal-crown.webp +0 -0
  21. package/assets/gold-sun-medallion.webp +0 -0
  22. package/assets/graduation-cap.webp +0 -0
  23. package/assets/green-square-glasses.webp +0 -0
  24. package/assets/layers.css +159 -0
  25. package/assets/leather-bell.webp +0 -0
  26. package/assets/lilac-flower-crown.webp +0 -0
  27. package/assets/manifest.json +769 -0
  28. package/assets/matte-black-top-hat.webp +0 -0
  29. package/assets/navy-wizard-hat.webp +0 -0
  30. package/assets/orange-hard-hat.webp +0 -0
  31. package/assets/plum-glasses.webp +0 -0
  32. package/assets/purple-scarf.webp +0 -0
  33. package/assets/purple-witch-hat.webp +0 -0
  34. package/assets/rainbow-propeller-cap.webp +0 -0
  35. package/assets/rainbow-visor.webp +0 -0
  36. package/assets/ranger-fedora-feather.webp +0 -0
  37. package/assets/red-polka-bowtie.webp +0 -0
  38. package/assets/retro-3d-glasses.webp +0 -0
  39. package/assets/rose-heart-sunglasses.webp +0 -0
  40. package/assets/royal-crown-rubies.webp +0 -0
  41. package/assets/sailor-cap-anchor.webp +0 -0
  42. package/assets/silver-space-helmet.webp +0 -0
  43. package/assets/straw-boater.webp +0 -0
  44. package/assets/teal-bowtie.webp +0 -0
  45. package/assets/teal-glasses.webp +0 -0
  46. package/assets/terracotta-beanie.webp +0 -0
  47. package/assets/tortoiseshell-readers.webp +0 -0
  48. package/assets/turquoise-bandana.webp +0 -0
  49. package/assets/violet-masquerade-mask.webp +0 -0
  50. package/assets/white-snow-goggles.webp +0 -0
  51. package/assets/white-tennis-visor.webp +0 -0
  52. package/assets/witch-hat-moon.webp +0 -0
  53. package/assets/yellow-star-glasses.webp +0 -0
  54. package/frockbot.json +38 -0
  55. package/package.json +47 -6
  56. package/src/agent.test.ts +401 -0
  57. package/src/agent.ts +571 -0
  58. package/src/assets.test.ts +31 -0
  59. package/src/backend.test.ts +205 -0
  60. package/src/backend.ts +283 -0
  61. package/src/bot.test.ts +190 -0
  62. package/src/bot.ts +255 -0
  63. package/src/client/BotAvatar.vue +20 -0
  64. package/src/client/FlockAvatar.vue +33 -0
  65. package/src/client/FlockAvatarEditor.vue +44 -0
  66. package/src/client/FlockCreateButton.vue +18 -0
  67. package/src/client/FlockIdentity.vue +31 -0
  68. package/src/client/FlockOverlay.vue +209 -0
  69. package/src/client/FlockSidebar.vue +254 -0
  70. package/src/client/SheepAvatar.vue +28 -0
  71. package/src/client/dialog-focus.test.ts +15 -0
  72. package/src/client/dialog-focus.ts +13 -0
  73. package/src/client/index.test.ts +502 -0
  74. package/src/client/index.ts +619 -0
  75. package/src/client/mobile-dialog-layout.test.ts +50 -0
  76. package/src/client/pending-create.test.ts +71 -0
  77. package/src/client/pending-create.ts +93 -0
  78. package/src/client/sidebar.test.ts +77 -0
  79. package/src/client/sidebar.ts +77 -0
  80. package/src/client/state.ts +53 -0
  81. package/src/client/styles.css +611 -0
  82. package/src/env.d.ts +9 -0
  83. package/src/index.ts +4 -0
  84. package/src/manifest.ts +3 -0
  85. package/src/package.test.ts +12 -0
  86. package/src/shared.test.ts +197 -0
  87. package/src/shared.ts +735 -0
  88. package/src/user.test.ts +455 -0
  89. package/src/user.ts +465 -0
  90. package/tsconfig.json +15 -0
  91. package/validate_readability.py +64 -0
  92. package/vite.config.ts +30 -0
package/src/bot.ts ADDED
@@ -0,0 +1,255 @@
1
+ import type { Plugin } from "cordis";
2
+ import {
3
+ FlockConflictError,
4
+ FlockDecodeError,
5
+ decodeBotLifecycleCommandV1,
6
+ decodeBotLifecycleReceiptV1,
7
+ decodeBotLifecycleViewV1,
8
+ decodeBotRegistrationV1,
9
+ decodeSheepIdentityViewV1,
10
+ decodeStoredBotLifecycleReceiptV1,
11
+ decodeStoredFlockReceiptV1,
12
+ flockCommandFingerprint,
13
+ type BotLifecycleCommandV1,
14
+ type BotLifecycleReceiptV1,
15
+ type BotLifecycleViewV1,
16
+ type BotRegistrationV1,
17
+ type FlockReceiptV1,
18
+ type SheepIdentityViewV1,
19
+ type UpdateSheepCommandV1,
20
+ } from "./shared.js";
21
+
22
+ const IDENTITY_KEY = "flock:sheep:v1";
23
+ const RECEIPT_PREFIX = "flock:sheep-receipt:";
24
+ const LIFECYCLE_KEY = "flock:lifecycle:v1";
25
+ const LIFECYCLE_RECEIPT_PREFIX = "flock:lifecycle-receipt:";
26
+ export interface FlockBotTransaction {
27
+ get<T>(key: string): Promise<T | undefined>;
28
+ list<T>(options: { prefix: string }): Promise<Map<string, T>>;
29
+ put<T>(key: string, value: T): Promise<void>;
30
+ put(entries: Record<string, unknown>): Promise<void>;
31
+ }
32
+ interface Storage extends FlockBotTransaction {
33
+ transaction<T>(
34
+ callback: (storage: FlockBotTransaction) => Promise<T>,
35
+ ): Promise<T>;
36
+ }
37
+ export interface FlockBotBackendHost {
38
+ storage: Storage;
39
+ materializeSettings(
40
+ registration: BotRegistrationV1,
41
+ userId: string,
42
+ ): Promise<void>;
43
+ archiveEligible(storage: FlockBotTransaction): Promise<boolean>;
44
+ }
45
+
46
+ export class BotArchivedError extends Error {
47
+ constructor(readonly botId: string) {
48
+ super(`Bot "${botId}" is archived`);
49
+ this.name = "BotArchivedError";
50
+ }
51
+ }
52
+
53
+ export class FlockBotBackendContribution {
54
+ constructor(private readonly host: FlockBotBackendHost) {}
55
+
56
+ async materialize(
57
+ registrationInput: BotRegistrationV1,
58
+ userId: string,
59
+ ): Promise<SheepIdentityViewV1> {
60
+ const registration = decodeBotRegistrationV1(registrationInput);
61
+ await this.host.materializeSettings(registration, userId);
62
+ return this.host.storage.transaction(async (storage) => {
63
+ const existingValue = await storage.get<unknown>(IDENTITY_KEY);
64
+ const lifecycleValue = await storage.get<unknown>(LIFECYCLE_KEY);
65
+ const lifecycle =
66
+ lifecycleValue === undefined
67
+ ? {
68
+ schemaVersion: 1 as const,
69
+ botId: registration.botId,
70
+ status: "active" as const,
71
+ revision: 0,
72
+ }
73
+ : decodeBotLifecycleViewV1(lifecycleValue);
74
+ if (lifecycle.botId !== registration.botId)
75
+ throw new Error("Bot lifecycle does not match Bot registration");
76
+ const existing =
77
+ existingValue === undefined
78
+ ? undefined
79
+ : decodeSheepIdentityViewV1(existingValue);
80
+ if (existing) {
81
+ if (existing.botId !== registration.botId)
82
+ throw new Error("sheep identity does not match Bot registration");
83
+ if (lifecycleValue === undefined)
84
+ await storage.put(LIFECYCLE_KEY, lifecycle);
85
+ return existing;
86
+ }
87
+ const initial = {
88
+ schemaVersion: 1,
89
+ botId: registration.botId,
90
+ revision: 0,
91
+ sheep: structuredClone(registration.sheep),
92
+ } satisfies SheepIdentityViewV1;
93
+ await storage.put({
94
+ [IDENTITY_KEY]: initial,
95
+ [LIFECYCLE_KEY]: lifecycle,
96
+ });
97
+ return initial;
98
+ });
99
+ }
100
+
101
+ async read(
102
+ registration: BotRegistrationV1,
103
+ userId: string,
104
+ ): Promise<SheepIdentityViewV1> {
105
+ return structuredClone(await this.materialize(registration, userId));
106
+ }
107
+
108
+ async update(
109
+ registration: BotRegistrationV1,
110
+ userId: string,
111
+ command: UpdateSheepCommandV1,
112
+ ): Promise<FlockReceiptV1> {
113
+ if (registration.botId !== command.botId)
114
+ throw new Error("sheep command does not match Bot registration");
115
+ await this.materialize(registration, userId);
116
+ const fingerprint = flockCommandFingerprint(command);
117
+ return this.host.storage.transaction(async (storage) => {
118
+ await this.assertActive(storage, registration.botId);
119
+ const receiptKey = `${RECEIPT_PREFIX}${command.commandId}`;
120
+ const storedValue = await storage.get<unknown>(receiptKey);
121
+ const stored =
122
+ storedValue === undefined
123
+ ? undefined
124
+ : decodeStoredFlockReceiptV1(storedValue);
125
+ if (stored) {
126
+ if (stored.fingerprint !== fingerprint)
127
+ throw new FlockDecodeError(
128
+ `command ID collision: ${command.commandId}`,
129
+ );
130
+ return structuredClone(stored.receipt);
131
+ }
132
+ const currentValue = await storage.get<unknown>(IDENTITY_KEY);
133
+ if (currentValue === undefined)
134
+ throw new Error("sheep identity was not materialized");
135
+ const current = decodeSheepIdentityViewV1(currentValue);
136
+ if (current.revision !== command.expectedRevision)
137
+ throw new FlockConflictError(current.revision);
138
+ const next = {
139
+ ...current,
140
+ revision: current.revision + 1,
141
+ sheep: structuredClone(command.sheep),
142
+ } satisfies SheepIdentityViewV1;
143
+ const receipt = {
144
+ schemaVersion: 1,
145
+ commandId: command.commandId,
146
+ status: "applied",
147
+ revision: next.revision,
148
+ } satisfies FlockReceiptV1;
149
+ await storage.put({
150
+ [IDENTITY_KEY]: next,
151
+ [receiptKey]: { fingerprint, receipt },
152
+ });
153
+ return receipt;
154
+ });
155
+ }
156
+
157
+ async readLifecycle(
158
+ registration: BotRegistrationV1,
159
+ userId: string,
160
+ ): Promise<BotLifecycleViewV1> {
161
+ await this.materialize(registration, userId);
162
+ const stored = await this.host.storage.get<unknown>(LIFECYCLE_KEY);
163
+ if (stored === undefined)
164
+ throw new Error("Bot lifecycle was not materialized");
165
+ return structuredClone(decodeBotLifecycleViewV1(stored));
166
+ }
167
+
168
+ async executeLifecycle(
169
+ registration: BotRegistrationV1,
170
+ userId: string,
171
+ input: unknown,
172
+ ): Promise<BotLifecycleReceiptV1> {
173
+ const command = decodeBotLifecycleCommandV1(input);
174
+ if (registration.botId !== command.botId)
175
+ throw new Error("lifecycle command does not match Bot registration");
176
+ await this.materialize(registration, userId);
177
+ const fingerprint = flockCommandFingerprint(command);
178
+ return this.host.storage.transaction(async (storage) => {
179
+ const receiptKey = `${LIFECYCLE_RECEIPT_PREFIX}${command.commandId}`;
180
+ const storedReceipt = await storage.get<unknown>(receiptKey);
181
+ if (storedReceipt !== undefined) {
182
+ const decoded = decodeStoredBotLifecycleReceiptV1(storedReceipt);
183
+ if (decoded.fingerprint !== fingerprint)
184
+ throw new FlockDecodeError(
185
+ `command ID collision: ${command.commandId}`,
186
+ );
187
+ return structuredClone(decoded.receipt);
188
+ }
189
+ const currentValue = await storage.get<unknown>(LIFECYCLE_KEY);
190
+ if (currentValue === undefined)
191
+ throw new Error("Bot lifecycle was not materialized");
192
+ const current = decodeBotLifecycleViewV1(currentValue);
193
+ const target = command.type === "bot/archive" ? "archived" : "active";
194
+ let receipt: BotLifecycleReceiptV1;
195
+ if (
196
+ target === "archived" &&
197
+ current.status !== "archived" &&
198
+ !(await this.host.archiveEligible(storage))
199
+ ) {
200
+ receipt = {
201
+ schemaVersion: 1,
202
+ commandId: command.commandId,
203
+ botId: command.botId,
204
+ status: "rejected",
205
+ lifecycle: current,
206
+ failure: "Bot has active or reconciling work",
207
+ };
208
+ } else {
209
+ const lifecycle =
210
+ current.status === target
211
+ ? current
212
+ : ({
213
+ ...current,
214
+ status: target,
215
+ revision: current.revision + 1,
216
+ } satisfies BotLifecycleViewV1);
217
+ receipt = {
218
+ schemaVersion: 1,
219
+ commandId: command.commandId,
220
+ botId: command.botId,
221
+ status: "applied",
222
+ lifecycle,
223
+ };
224
+ await storage.put(LIFECYCLE_KEY, lifecycle);
225
+ }
226
+ await storage.put(receiptKey, { fingerprint, receipt });
227
+ return decodeBotLifecycleReceiptV1(receipt);
228
+ });
229
+ }
230
+
231
+ async assertActive(
232
+ storage: FlockBotTransaction,
233
+ botId: string,
234
+ ): Promise<void> {
235
+ const value = await storage.get<unknown>(LIFECYCLE_KEY);
236
+ if (value === undefined) return;
237
+ const lifecycle = decodeBotLifecycleViewV1(value);
238
+ if (lifecycle.botId !== botId)
239
+ throw new Error("Bot lifecycle identity does not match");
240
+ if (lifecycle.status === "archived") throw new BotArchivedError(botId);
241
+ }
242
+ }
243
+
244
+ export function createFlockBotBackendContribution(
245
+ host: FlockBotBackendHost,
246
+ ): FlockBotBackendContribution {
247
+ return new FlockBotBackendContribution(host);
248
+ }
249
+
250
+ export function createFlockBotBackendPlugin(
251
+ host: FlockBotBackendHost,
252
+ lifecycle: { mount(value: FlockBotBackendContribution): () => void },
253
+ ): Plugin {
254
+ return () => lifecycle.mount(createFlockBotBackendContribution(host));
255
+ }
@@ -0,0 +1,20 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * What a Bot looks like. The Flock's generated sheep recipe is the avatar.
4
+ */
5
+ import SheepAvatar from "./SheepAvatar.vue";
6
+ import type { SheepRecipeV1 } from "../shared.js";
7
+
8
+ const props = withDefaults(
9
+ defineProps<{
10
+ botId: string;
11
+ sheep: SheepRecipeV1;
12
+ label?: string;
13
+ size?: "mini" | "small" | "large";
14
+ }>(),
15
+ { label: "Bot avatar", size: "small" },
16
+ );
17
+ </script>
18
+ <template>
19
+ <SheepAvatar :sheep="sheep" :label="label" :size="size" />
20
+ </template>
@@ -0,0 +1,33 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject } from "vue";
3
+ import { frockBotWebDataKey } from "@frockbot/plugin-shell/shared";
4
+ import { flockWebDataKey } from "./state.js";
5
+ import BotAvatar from "./BotAvatar.vue";
6
+
7
+ /**
8
+ * The active Bot's sheep, rendered beside each assistant message. The shell
9
+ * owns the slot and the animation; the Flock Package owns what a Bot looks
10
+ * like, so the shell never imports this Package.
11
+ */
12
+ const flock = inject(flockWebDataKey);
13
+ const shell = inject(frockBotWebDataKey);
14
+ if (!flock || !shell) throw new Error("Flock client data was not provided");
15
+ const identity = computed(() =>
16
+ shell.value.activeBotId
17
+ ? flock.value.identities[shell.value.activeBotId]
18
+ : undefined,
19
+ );
20
+ const name = computed(
21
+ () => shell.value.botSettings?.profile.name ?? "This Bot",
22
+ );
23
+ </script>
24
+
25
+ <template>
26
+ <BotAvatar
27
+ v-if="identity && shell.activeBotId"
28
+ :bot-id="shell.activeBotId"
29
+ :sheep="identity.sheep"
30
+ size="mini"
31
+ :label="name"
32
+ />
33
+ </template>
@@ -0,0 +1,44 @@
1
+ <script setup lang="ts">
2
+ import { UiButton } from "@frockbot/client-ui";
3
+ import { frockBotWebDataKey } from "@frockbot/plugin-shell/shared";
4
+ import { computed, inject } from "vue";
5
+ import BotAvatar from "./BotAvatar.vue";
6
+ import { flockWebDataKey } from "./state.js";
7
+
8
+ const flock = inject(flockWebDataKey);
9
+ const shell = inject(frockBotWebDataKey);
10
+ if (!flock || !shell) throw new Error("Flock client data was not provided");
11
+ flock.value.bindShell(shell);
12
+
13
+ const identity = computed(() =>
14
+ shell.value.activeBotId
15
+ ? flock.value.identities[shell.value.activeBotId]
16
+ : undefined,
17
+ );
18
+ const label = computed(
19
+ () => `${shell.value.botSettings?.profile.name ?? "This Bot"} avatar`,
20
+ );
21
+ </script>
22
+
23
+ <template>
24
+ <div v-if="identity && shell.activeBotId" class="flock-avatar-editor">
25
+ <BotAvatar
26
+ :bot-id="shell.activeBotId"
27
+ :sheep="identity.sheep"
28
+ :label="label"
29
+ size="large"
30
+ />
31
+ <UiButton type="button" variant="ghost" @click="flock.openEdit">
32
+ Change
33
+ </UiButton>
34
+ </div>
35
+ </template>
36
+
37
+ <style scoped>
38
+ .flock-avatar-editor {
39
+ display: flex;
40
+ flex-direction: column;
41
+ align-items: center;
42
+ gap: 12px;
43
+ }
44
+ </style>
@@ -0,0 +1,18 @@
1
+ <script setup lang="ts">
2
+ import { UiIconButton } from "@frockbot/client-ui";
3
+ import { inject } from "vue";
4
+ import { flockWebDataKey } from "./state.js";
5
+
6
+ const flock = inject(flockWebDataKey);
7
+ if (!flock) throw new Error("Flock client data was not provided");
8
+ </script>
9
+
10
+ <template>
11
+ <UiIconButton
12
+ class="flock-create-button"
13
+ icon="plus"
14
+ label="Create Bot"
15
+ size="md"
16
+ @click="flock.openCreate"
17
+ />
18
+ </template>
@@ -0,0 +1,31 @@
1
+ <script setup lang="ts">
2
+ import { computed, inject } from "vue";
3
+ import { frockBotWebDataKey } from "@frockbot/plugin-shell/shared";
4
+ import { flockWebDataKey } from "./state.js";
5
+ import BotAvatar from "./BotAvatar.vue";
6
+ const flock = inject(flockWebDataKey);
7
+ const shell = inject(frockBotWebDataKey);
8
+ if (!flock || !shell) throw new Error("Flock client data was not provided");
9
+ flock.value.bindShell(shell);
10
+ const identity = computed(() =>
11
+ shell.value.activeBotId
12
+ ? flock.value.identities[shell.value.activeBotId]
13
+ : undefined,
14
+ );
15
+ </script>
16
+ <template>
17
+ <button
18
+ v-if="identity"
19
+ class="flock-identity-button"
20
+ type="button"
21
+ title="Tailor sheep"
22
+ aria-label="Tailor sheep"
23
+ @click="flock.openEdit"
24
+ >
25
+ <BotAvatar
26
+ :bot-id="shell.activeBotId!"
27
+ :sheep="identity.sheep"
28
+ :label="`${shell.botSettings?.profile.name ?? 'This Bot'} avatar`"
29
+ /></button
30
+ ><span v-else>⌁</span>
31
+ </template>
@@ -0,0 +1,209 @@
1
+ <script setup lang="ts">
2
+ import { inject, nextTick, onBeforeUnmount, ref, watch } from "vue";
3
+ import { sheepCatalog } from "../shared.js";
4
+ import { flockWebDataKey } from "./state.js";
5
+ import { dialogFocusWrapTarget } from "./dialog-focus.js";
6
+ import SheepAvatar from "./SheepAvatar.vue";
7
+ const providedFlock = inject(flockWebDataKey);
8
+ if (!providedFlock) throw new Error("Flock client data was not provided");
9
+ const flock = providedFlock;
10
+
11
+ const dialog = ref<HTMLElement>();
12
+ let restoreFocus: HTMLElement | undefined;
13
+ const focusable =
14
+ 'button:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
15
+
16
+ watch(
17
+ () => flock.value.overlay,
18
+ async (overlay, previous) => {
19
+ if (overlay && !previous) {
20
+ restoreFocus =
21
+ document.activeElement instanceof HTMLElement
22
+ ? document.activeElement
23
+ : undefined;
24
+ await nextTick();
25
+ dialog.value
26
+ ?.querySelector<HTMLElement>("[autofocus], " + focusable)
27
+ ?.focus();
28
+ } else if (!overlay && previous) {
29
+ restoreFocus?.focus();
30
+ restoreFocus = undefined;
31
+ }
32
+ },
33
+ { flush: "post" },
34
+ );
35
+
36
+ function onDialogKeydown(event: KeyboardEvent): void {
37
+ if (event.key === "Escape") {
38
+ event.preventDefault();
39
+ flock.value.closeOverlay();
40
+ return;
41
+ }
42
+ if (event.key !== "Tab" || !dialog.value) return;
43
+ const controls = [...dialog.value.querySelectorAll<HTMLElement>(focusable)];
44
+ if (controls.length === 0) return;
45
+ const target = dialogFocusWrapTarget(
46
+ controls,
47
+ document.activeElement as HTMLElement | null,
48
+ event.shiftKey,
49
+ );
50
+ if (!target) return;
51
+ event.preventDefault();
52
+ target.focus();
53
+ }
54
+
55
+ onBeforeUnmount(() => restoreFocus?.focus());
56
+ </script>
57
+ <template>
58
+ <div
59
+ v-if="flock.overlay"
60
+ class="flock-backdrop"
61
+ @click.self="flock.closeOverlay"
62
+ >
63
+ <section
64
+ ref="dialog"
65
+ class="flock-dialog"
66
+ role="dialog"
67
+ aria-modal="true"
68
+ aria-labelledby="flock-title"
69
+ @keydown="onDialogKeydown"
70
+ >
71
+ <div v-if="flock.overlay === 'archive'" class="flock-form">
72
+ <span class="flock-eyebrow">Archive Bot</span>
73
+ <h1 id="flock-title">Archive this Bot?</h1>
74
+ <p>
75
+ Archiving stops new work and hides the Bot from your active flock.
76
+ History, settings, and Assignments are preserved for restoration.
77
+ </p>
78
+ <p
79
+ v-if="flock.error"
80
+ class="flock-error"
81
+ role="alert"
82
+ aria-live="assertive"
83
+ >
84
+ {{ flock.error }}
85
+ </p>
86
+ <div class="flock-actions">
87
+ <button type="button" @click="flock.closeOverlay">Cancel</button>
88
+ <button class="primary" type="button" @click="flock.archive">
89
+ Archive Bot
90
+ </button>
91
+ </div>
92
+ </div>
93
+ <template v-else>
94
+ <div class="flock-preview">
95
+ <SheepAvatar
96
+ :sheep="flock.draftSheep"
97
+ :label="
98
+ flock.draftName
99
+ ? `${flock.draftName} sheep preview`
100
+ : 'Sheep preview'
101
+ "
102
+ size="large"
103
+ />
104
+ <h2>Meet your sheep</h2>
105
+ <p>Randomly tailored. Entirely yours.</p>
106
+ <button type="button" class="flock-reroll" @click="flock.reroll">
107
+ ↻ Surprise me
108
+ </button>
109
+ </div>
110
+ <form
111
+ class="flock-form"
112
+ @submit.prevent="
113
+ flock.overlay === 'create' ? flock.create() : flock.saveSheep()
114
+ "
115
+ >
116
+ <span class="flock-eyebrow">{{
117
+ flock.overlay === "create" ? "Create a Bot" : "Tailor your Bot"
118
+ }}</span>
119
+ <h1 id="flock-title">
120
+ {{
121
+ flock.overlay === "create"
122
+ ? "Add to your flock"
123
+ : "Change the look"
124
+ }}
125
+ </h1>
126
+ <p>
127
+ Keep this look or tailor each layer. Your choice is saved with the
128
+ Bot.
129
+ </p>
130
+ <label v-if="flock.overlay === 'create'" class="flock-name"
131
+ >Bot name<input
132
+ v-model.trim="flock.draftName"
133
+ autofocus
134
+ maxlength="100"
135
+ required
136
+ autocomplete="off"
137
+ /></label>
138
+ <fieldset>
139
+ <legend>Sheep wardrobe</legend>
140
+ <div class="flock-select-grid">
141
+ <label
142
+ >Background<select v-model="flock.draftSheep.background">
143
+ <option
144
+ v-for="item in sheepCatalog.backgrounds"
145
+ :key="item.id"
146
+ :value="item.id"
147
+ >
148
+ {{ item.label }}
149
+ </option>
150
+ </select></label
151
+ >
152
+ <label
153
+ >Headwear<select v-model="flock.draftSheep.upper">
154
+ <option
155
+ v-for="item in sheepCatalog.trees.upper"
156
+ :key="item.id"
157
+ :value="item.id"
158
+ >
159
+ {{ item.label }}
160
+ </option>
161
+ </select></label
162
+ >
163
+ <label
164
+ >Face<select v-model="flock.draftSheep.middle">
165
+ <option
166
+ v-for="item in sheepCatalog.trees.middle"
167
+ :key="item.id"
168
+ :value="item.id"
169
+ >
170
+ {{ item.label }}
171
+ </option>
172
+ </select></label
173
+ >
174
+ <label
175
+ >Neckwear<select v-model="flock.draftSheep.lower">
176
+ <option
177
+ v-for="item in sheepCatalog.trees.lower"
178
+ :key="item.id"
179
+ :value="item.id"
180
+ >
181
+ {{ item.label }}
182
+ </option>
183
+ </select></label
184
+ >
185
+ </div>
186
+ </fieldset>
187
+ <div class="flock-note">
188
+ This identity is durable. Refreshing or switching devices won’t
189
+ re-roll it.
190
+ </div>
191
+ <p
192
+ v-if="flock.error"
193
+ class="flock-error"
194
+ role="alert"
195
+ aria-live="assertive"
196
+ >
197
+ {{ flock.error }}
198
+ </p>
199
+ <div class="flock-actions">
200
+ <button type="button" @click="flock.closeOverlay">Cancel</button
201
+ ><button class="primary" type="submit">
202
+ {{ flock.overlay === "create" ? "Create Bot" : "Save look" }}
203
+ </button>
204
+ </div>
205
+ </form>
206
+ </template>
207
+ </section>
208
+ </div>
209
+ </template>