@frockbot/plugin-shell 0.3.1 → 0.3.3
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 +32 -29
- package/src/agent.test.ts +15 -3
- package/src/backend-applets.test.ts +581 -0
- package/src/backend-applets.ts +959 -0
- package/src/backend-authoring.test.ts +61 -19
- package/src/backend-authoring.ts +66 -27
- package/src/backend-completion.ts +4 -2
- package/src/backend-composition.ts +64 -0
- package/src/backend-computer.test.ts +128 -0
- package/src/backend-computer.ts +81 -0
- package/src/backend-configuration.test.ts +8 -1
- package/src/backend-iframe-ui.test.ts +29 -12
- package/src/backend-isolate.ts +31 -5
- package/src/backend-package-catalog.test.ts +13 -8
- package/src/backend-package-catalog.ts +8 -6
- package/src/backend-recovery-integration.test.ts +23 -0
- package/src/backend-recovery.ts +20 -12
- package/src/backend-runner-iframe.test.ts +10 -1
- package/src/backend-runner.ts +9 -2
- package/src/backend-stop.test.ts +6 -6
- package/src/backend-supersede.test.ts +377 -0
- package/src/backend.ts +567 -13
- package/src/client/AppletCanvas.vue +679 -0
- package/src/client/FrockBotApp.vue +195 -21
- package/src/client/PackageEntryTrigger.vue +77 -0
- package/src/client/PackageIframeHost.vue +148 -47
- package/src/client/PackageIframeSettings.vue +8 -6
- package/src/client/PackageSurfacePage.vue +39 -0
- package/src/client/applets-client.test.ts +204 -0
- package/src/client/applets-client.ts +139 -0
- package/src/client/applets-state.ts +64 -0
- package/src/client/index.test.ts +221 -7
- package/src/client/index.ts +398 -6
- package/src/client/package-iframe-entries.test.ts +122 -0
- package/src/client/package-iframe-entries.ts +112 -0
- package/src/client/package-iframe-host-message.test.ts +3 -3
- package/src/client/package-iframe-host-message.ts +3 -3
- package/src/client/styles.css +118 -1
- package/src/composition-views.ts +31 -6
- package/src/run-protocol.test.ts +92 -0
- package/src/run-protocol.ts +193 -17
- package/src/shared.ts +70 -0
- package/src/terminal-records.test.ts +52 -1
- package/src/terminal-records.ts +48 -0
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from "../shared.js";
|
|
28
28
|
import { ComposerDraftStore } from "./composer-draft.js";
|
|
29
29
|
import SendPayloadView from "./SendPayloadView.vue";
|
|
30
|
+
import AppletCanvas from "./AppletCanvas.vue";
|
|
30
31
|
import PackageIframeHost from "./PackageIframeHost.vue";
|
|
31
32
|
import type { ClientSkillCatalogEntryV1 } from "../skill-protocol.js";
|
|
32
33
|
import {
|
|
@@ -127,11 +128,113 @@ function closeDrawers(): void {
|
|
|
127
128
|
if (!panelSurface.value) rightPanelOpen.value = false;
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
/*
|
|
132
|
+
* Escape, wherever the focus is.
|
|
133
|
+
*
|
|
134
|
+
* A drawer's own trigger disappears when the drawer opens, so focus can land
|
|
135
|
+
* back on the document body — outside this component's element — and a handler
|
|
136
|
+
* bound to the root would never see the key. The window is where "give the
|
|
137
|
+
* conversation back" has to be heard.
|
|
138
|
+
*/
|
|
130
139
|
function onRootKeydown(event: KeyboardEvent): void {
|
|
131
|
-
if (event.key !== "Escape" ||
|
|
140
|
+
if (event.key !== "Escape" || event.defaultPrevented) return;
|
|
141
|
+
if (navOpen.value) {
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
closeNav();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
// An open overlay owns Escape; closing the panel underneath it would be a
|
|
147
|
+
// second dismissal the User did not ask for.
|
|
148
|
+
if (overlaySurface.value) return;
|
|
149
|
+
// On a phone the right panel covers the conversation, so Escape gives the
|
|
150
|
+
// conversation back — the same thing tapping the scrim does.
|
|
151
|
+
if (phoneLayout.value && rightPanelOpen.value && !panelSurface.value) {
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
rightPanelOpen.value = false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/*
|
|
158
|
+
* The Applet canvas.
|
|
159
|
+
*
|
|
160
|
+
* A Session with a focused Applet gives the right panel to the canvas; without
|
|
161
|
+
* one the panel keeps the content its plugins put there. The canvas is wider
|
|
162
|
+
* than a summary column, and how much wider is the User's: the width they drag
|
|
163
|
+
* is theirs and is remembered per browser, which is a per-viewer convenience
|
|
164
|
+
* rather than durable state and belongs in local storage.
|
|
165
|
+
*/
|
|
166
|
+
const APPLET_PANEL_WIDTH_KEY = "frockbot.applet-panel-width";
|
|
167
|
+
const APPLET_PANEL_MIN = 320;
|
|
168
|
+
const APPLET_PANEL_MAX = 900;
|
|
169
|
+
const APPLET_PANEL_DEFAULT = 480;
|
|
170
|
+
|
|
171
|
+
function readStoredPanelWidth(): number {
|
|
172
|
+
try {
|
|
173
|
+
const stored = Number(window.localStorage.getItem(APPLET_PANEL_WIDTH_KEY));
|
|
174
|
+
if (!Number.isFinite(stored) || stored <= 0) return APPLET_PANEL_DEFAULT;
|
|
175
|
+
return Math.min(APPLET_PANEL_MAX, Math.max(APPLET_PANEL_MIN, stored));
|
|
176
|
+
} catch {
|
|
177
|
+
return APPLET_PANEL_DEFAULT;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const appletPanelWidth = ref(
|
|
182
|
+
typeof window === "undefined" ? APPLET_PANEL_DEFAULT : readStoredPanelWidth(),
|
|
183
|
+
);
|
|
184
|
+
const appletCanvasOpen = computed(() =>
|
|
185
|
+
Boolean(state.value.focusedAppletId && !panelSurface.value),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
function storePanelWidth(width: number): void {
|
|
189
|
+
try {
|
|
190
|
+
window.localStorage.setItem(APPLET_PANEL_WIDTH_KEY, String(width));
|
|
191
|
+
} catch {
|
|
192
|
+
// A browser that refuses storage still resizes; it just forgets.
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function setPanelWidth(width: number): void {
|
|
197
|
+
appletPanelWidth.value = Math.min(
|
|
198
|
+
APPLET_PANEL_MAX,
|
|
199
|
+
Math.max(APPLET_PANEL_MIN, Math.round(width)),
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function onPanelHandlePointerDown(event: PointerEvent): void {
|
|
204
|
+
if (phoneLayout.value) return;
|
|
205
|
+
const target = event.currentTarget as HTMLElement;
|
|
206
|
+
target.setPointerCapture(event.pointerId);
|
|
207
|
+
const move = (moveEvent: PointerEvent) => {
|
|
208
|
+
setPanelWidth(window.innerWidth - moveEvent.clientX);
|
|
209
|
+
};
|
|
210
|
+
const stop = () => {
|
|
211
|
+
target.removeEventListener("pointermove", move);
|
|
212
|
+
target.removeEventListener("pointerup", stop);
|
|
213
|
+
target.removeEventListener("pointercancel", stop);
|
|
214
|
+
storePanelWidth(appletPanelWidth.value);
|
|
215
|
+
};
|
|
216
|
+
target.addEventListener("pointermove", move);
|
|
217
|
+
target.addEventListener("pointerup", stop);
|
|
218
|
+
target.addEventListener("pointercancel", stop);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The keyboard's way to do what the drag does. */
|
|
222
|
+
function onPanelHandleKeydown(event: KeyboardEvent): void {
|
|
223
|
+
const step = event.shiftKey ? 64 : 16;
|
|
224
|
+
if (event.key === "ArrowLeft") setPanelWidth(appletPanelWidth.value + step);
|
|
225
|
+
else if (event.key === "ArrowRight")
|
|
226
|
+
setPanelWidth(appletPanelWidth.value - step);
|
|
227
|
+
else return;
|
|
132
228
|
event.preventDefault();
|
|
133
|
-
|
|
229
|
+
storePanelWidth(appletPanelWidth.value);
|
|
134
230
|
}
|
|
231
|
+
|
|
232
|
+
/** The phone's way into a focused Applet while the panel is closed. */
|
|
233
|
+
const appletChip = computed(() =>
|
|
234
|
+
phoneLayout.value && !rightPanelOpen.value && state.value.focusedAppletId
|
|
235
|
+
? (state.value.focusedApplet?.displayName ?? "Applet")
|
|
236
|
+
: undefined,
|
|
237
|
+
);
|
|
135
238
|
/*
|
|
136
239
|
* Skill invocation. `/` or `@` at a word boundary opens a popover over the
|
|
137
240
|
* Bot's catalog; choosing one attaches a ref chip and removes the trigger from
|
|
@@ -167,16 +270,26 @@ const macDesktop =
|
|
|
167
270
|
const botName = computed(
|
|
168
271
|
() => state.value.botSettings?.profile.name ?? "Barebones",
|
|
169
272
|
);
|
|
170
|
-
|
|
273
|
+
/** A Turn is executing. The composer stays open; only Stop depends on this. */
|
|
274
|
+
const isRunning = computed(() => Boolean(state.value.runningRunId));
|
|
171
275
|
const isConnecting = computed(() => state.value.connection !== "ready");
|
|
276
|
+
/**
|
|
277
|
+
* Sending while the Bot is working is the point: the message supersedes the
|
|
278
|
+
* running Turn. So the only things that close the composer are the ones that
|
|
279
|
+
* would make any message impossible.
|
|
280
|
+
*/
|
|
172
281
|
const canSend = computed(
|
|
173
282
|
() =>
|
|
174
283
|
state.value.connection === "ready" &&
|
|
175
284
|
state.value.modelReady &&
|
|
176
285
|
Boolean(state.value.activeBotId) &&
|
|
177
|
-
!isRunning.value &&
|
|
178
286
|
draft.value.trim().length > 0,
|
|
179
287
|
);
|
|
288
|
+
/**
|
|
289
|
+
* Stop takes the button only while there is nothing to send. The moment the
|
|
290
|
+
* User has typed something, sending it is what they mean by interrupting.
|
|
291
|
+
*/
|
|
292
|
+
const showStop = computed(() => isRunning.value && !canSend.value);
|
|
180
293
|
|
|
181
294
|
/*
|
|
182
295
|
* Tool activity is internal to the Turn. A Turn that produced only tool calls
|
|
@@ -192,12 +305,26 @@ function attachmentsOf(message: WebChatMessage): WebToolAttachment[] {
|
|
|
192
305
|
}
|
|
193
306
|
|
|
194
307
|
function iframeEntriesFor(tool: WebToolActivity) {
|
|
195
|
-
const
|
|
308
|
+
const separator = tool.name.indexOf("/");
|
|
309
|
+
const namespace = separator < 0 ? undefined : tool.name.slice(0, separator);
|
|
310
|
+
const toolName = separator < 0 ? tool.name : tool.name.slice(separator + 1);
|
|
311
|
+
const slot = `frockbot.tool-result:${toolName}`;
|
|
196
312
|
return (state.value.packageUi?.contributions ?? [])
|
|
313
|
+
.filter(
|
|
314
|
+
(contribution) =>
|
|
315
|
+
namespace === undefined || contribution.packageId === namespace,
|
|
316
|
+
)
|
|
197
317
|
.flatMap((contribution) =>
|
|
198
|
-
contribution.
|
|
199
|
-
.
|
|
200
|
-
|
|
318
|
+
contribution.pages.flatMap((page) =>
|
|
319
|
+
page.mounts
|
|
320
|
+
.filter((mount) => mount.slot === slot)
|
|
321
|
+
.map((mount) => ({
|
|
322
|
+
contribution,
|
|
323
|
+
page,
|
|
324
|
+
slot,
|
|
325
|
+
order: mount.order ?? 0,
|
|
326
|
+
})),
|
|
327
|
+
),
|
|
201
328
|
)
|
|
202
329
|
.sort(
|
|
203
330
|
(left, right) =>
|
|
@@ -387,12 +514,14 @@ onMounted(() => {
|
|
|
387
514
|
window.addEventListener("popstate", applySettingsDeepLink);
|
|
388
515
|
window.addEventListener("hashchange", applySettingsDeepLink);
|
|
389
516
|
phoneLayoutMedia?.addEventListener("change", onPhoneLayoutChange);
|
|
517
|
+
window.addEventListener("keydown", onRootKeydown);
|
|
390
518
|
});
|
|
391
519
|
|
|
392
520
|
onBeforeUnmount(() => {
|
|
393
521
|
window.removeEventListener("popstate", applySettingsDeepLink);
|
|
394
522
|
window.removeEventListener("hashchange", applySettingsDeepLink);
|
|
395
523
|
phoneLayoutMedia?.removeEventListener("change", onPhoneLayoutChange);
|
|
524
|
+
window.removeEventListener("keydown", onRootKeydown);
|
|
396
525
|
});
|
|
397
526
|
|
|
398
527
|
watch(
|
|
@@ -584,16 +713,18 @@ function handleComposerKeydown(event: KeyboardEvent): void {
|
|
|
584
713
|
</script>
|
|
585
714
|
|
|
586
715
|
<template>
|
|
587
|
-
<div class="frockbot-root"
|
|
716
|
+
<div class="frockbot-root">
|
|
588
717
|
<div
|
|
589
718
|
class="app-shell"
|
|
590
719
|
:class="{
|
|
591
720
|
'panel-open': rightPanelOpen,
|
|
592
721
|
'panel-surface': Boolean(panelSurface),
|
|
722
|
+
'panel-applet': appletCanvasOpen,
|
|
593
723
|
'mac-desktop': macDesktop,
|
|
594
724
|
'phone-layout': phoneLayout,
|
|
595
725
|
'nav-open': navOpen,
|
|
596
726
|
}"
|
|
727
|
+
:style="{ '--applet-panel-width': `${appletPanelWidth}px` }"
|
|
597
728
|
>
|
|
598
729
|
<aside
|
|
599
730
|
class="sidebar"
|
|
@@ -683,7 +814,10 @@ function handleComposerKeydown(event: KeyboardEvent): void {
|
|
|
683
814
|
:id="turnAnchors.get(message.id)"
|
|
684
815
|
:key="message.id"
|
|
685
816
|
class="message"
|
|
686
|
-
:class="
|
|
817
|
+
:class="[
|
|
818
|
+
`message-${message.role}`,
|
|
819
|
+
{ 'message-pending': message.pending },
|
|
820
|
+
]"
|
|
687
821
|
>
|
|
688
822
|
<p v-if="message.role === 'system'" class="message-system-line">
|
|
689
823
|
{{ message.text }}
|
|
@@ -713,12 +847,12 @@ function handleComposerKeydown(event: KeyboardEvent): void {
|
|
|
713
847
|
<template v-for="tool in message.tools" :key="tool.id">
|
|
714
848
|
<PackageIframeHost
|
|
715
849
|
v-for="entry in iframeEntriesFor(tool)"
|
|
716
|
-
:key="`${tool.id}:${entry.contribution.packageId}`"
|
|
850
|
+
:key="`${tool.id}:${entry.contribution.packageId}:${entry.page.id}`"
|
|
717
851
|
class="message-package-iframe"
|
|
718
852
|
:contribution="entry.contribution"
|
|
853
|
+
:page="entry.page"
|
|
719
854
|
:slot="entry.slot"
|
|
720
|
-
:
|
|
721
|
-
:state-value="toolResultState(tool)"
|
|
855
|
+
:states="{ [`tool:${tool.name}`]: toolResultState(tool) }"
|
|
722
856
|
/>
|
|
723
857
|
</template>
|
|
724
858
|
<!--
|
|
@@ -870,6 +1004,21 @@ function handleComposerKeydown(event: KeyboardEvent): void {
|
|
|
870
1004
|
</span>
|
|
871
1005
|
</li>
|
|
872
1006
|
</ul>
|
|
1007
|
+
<!--
|
|
1008
|
+
The phone's way back to a focused Applet. The panel is a drawer
|
|
1009
|
+
here, so with it closed there is nothing on screen that says an
|
|
1010
|
+
Applet is in play; this chip both says so and opens it.
|
|
1011
|
+
-->
|
|
1012
|
+
<button
|
|
1013
|
+
v-if="appletChip"
|
|
1014
|
+
type="button"
|
|
1015
|
+
class="applet-chip"
|
|
1016
|
+
@click="toggleRightPanel"
|
|
1017
|
+
>
|
|
1018
|
+
<UiIcon name="applets" size="sm" />
|
|
1019
|
+
<span class="applet-chip-name">Applet: {{ appletChip }}</span>
|
|
1020
|
+
<span class="applet-chip-action">Open</span>
|
|
1021
|
+
</button>
|
|
873
1022
|
<div class="composer-body">
|
|
874
1023
|
<ul v-if="attachedSkills.length > 0" class="skill-chips">
|
|
875
1024
|
<li v-for="entry in attachedSkills" :key="entry.ref">
|
|
@@ -896,7 +1045,7 @@ function handleComposerKeydown(event: KeyboardEvent): void {
|
|
|
896
1045
|
? 'Model unavailable'
|
|
897
1046
|
: `Message ${botName}`
|
|
898
1047
|
"
|
|
899
|
-
:disabled="isConnecting || !state.modelReady
|
|
1048
|
+
:disabled="isConnecting || !state.modelReady"
|
|
900
1049
|
rows="1"
|
|
901
1050
|
role="combobox"
|
|
902
1051
|
:aria-expanded="skillPopoverOpen"
|
|
@@ -909,7 +1058,7 @@ function handleComposerKeydown(event: KeyboardEvent): void {
|
|
|
909
1058
|
/>
|
|
910
1059
|
</div>
|
|
911
1060
|
<UiIconButton
|
|
912
|
-
v-if="
|
|
1061
|
+
v-if="showStop"
|
|
913
1062
|
class="stop-button"
|
|
914
1063
|
icon="stop"
|
|
915
1064
|
label="Stop generating"
|
|
@@ -936,15 +1085,40 @@ function handleComposerKeydown(event: KeyboardEvent): void {
|
|
|
936
1085
|
Both layers live in one stack so panel plugins keep their state
|
|
937
1086
|
while a surface holds their place.
|
|
938
1087
|
-->
|
|
1088
|
+
<!--
|
|
1089
|
+
The edge the User drags to make room for an Applet. It is a real
|
|
1090
|
+
control, not a hairline: it takes focus and the arrow keys do what
|
|
1091
|
+
the drag does.
|
|
1092
|
+
-->
|
|
1093
|
+
<div
|
|
1094
|
+
v-if="appletCanvasOpen && !phoneLayout"
|
|
1095
|
+
class="applet-panel-handle"
|
|
1096
|
+
role="separator"
|
|
1097
|
+
tabindex="0"
|
|
1098
|
+
aria-orientation="vertical"
|
|
1099
|
+
aria-label="Resize the Applet panel"
|
|
1100
|
+
:aria-valuenow="appletPanelWidth"
|
|
1101
|
+
:aria-valuemin="320"
|
|
1102
|
+
:aria-valuemax="900"
|
|
1103
|
+
@pointerdown="onPanelHandlePointerDown"
|
|
1104
|
+
@keydown="onPanelHandleKeydown"
|
|
1105
|
+
/>
|
|
939
1106
|
<div class="right-panel-stack">
|
|
940
1107
|
<Transition name="panel-swap">
|
|
941
1108
|
<div v-show="!panelSurface" class="right-panel-content">
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1109
|
+
<!--
|
|
1110
|
+
A focused Applet takes the panel; with none, the panel is the
|
|
1111
|
+
one its plugins have always drawn.
|
|
1112
|
+
-->
|
|
1113
|
+
<AppletCanvas v-if="appletCanvasOpen" />
|
|
1114
|
+
<template v-else>
|
|
1115
|
+
<header class="right-panel-header">
|
|
1116
|
+
<k-slot name="frockbot.bot-actions" />
|
|
1117
|
+
</header>
|
|
1118
|
+
<div class="right-panel-body">
|
|
1119
|
+
<k-slot name="frockbot.right-panel" />
|
|
1120
|
+
</div>
|
|
1121
|
+
</template>
|
|
948
1122
|
</div>
|
|
949
1123
|
</Transition>
|
|
950
1124
|
<Transition name="panel-swap">
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* One declarative Package entry in the sidebar.
|
|
4
|
+
*
|
|
5
|
+
* Everything drawn here is manifest data — the label, the icon name, and the
|
|
6
|
+
* page the entry opens — so a Package reaches the sidebar without running any
|
|
7
|
+
* code in the app origin. The icon is looked up in the shared set; a Package
|
|
8
|
+
* naming an icon this client does not have falls back to the generic one
|
|
9
|
+
* rather than drawing nothing.
|
|
10
|
+
*/
|
|
11
|
+
import { clientSurfaceRegistryKey } from "@frockbot/client-core";
|
|
12
|
+
import { UiIcon, uiIconPaths, type UiIconName } from "@frockbot/client-ui";
|
|
13
|
+
import { computed, inject } from "vue";
|
|
14
|
+
import type { PackageIframeEntryV1 } from "./package-iframe-entries.js";
|
|
15
|
+
|
|
16
|
+
const props = defineProps<{ entry: PackageIframeEntryV1 }>();
|
|
17
|
+
const surfaces = inject(clientSurfaceRegistryKey);
|
|
18
|
+
if (!surfaces) throw new Error("client surface registry was not provided");
|
|
19
|
+
|
|
20
|
+
const icon = computed<UiIconName>(() =>
|
|
21
|
+
Object.hasOwn(uiIconPaths, props.entry.entry.icon)
|
|
22
|
+
? (props.entry.entry.icon as UiIconName)
|
|
23
|
+
: "plugins",
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
function open(): void {
|
|
27
|
+
if (surfaces?.has(props.entry.surfaceId))
|
|
28
|
+
surfaces.open(props.entry.surfaceId);
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<button class="package-entry-trigger" type="button" @click="open">
|
|
34
|
+
<span class="package-entry-trigger__icon"><UiIcon :name="icon" /></span>
|
|
35
|
+
{{ entry.entry.label }}
|
|
36
|
+
</button>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<style scoped>
|
|
40
|
+
.package-entry-trigger {
|
|
41
|
+
display: flex;
|
|
42
|
+
width: 100%;
|
|
43
|
+
height: 40px;
|
|
44
|
+
align-items: center;
|
|
45
|
+
gap: 10px;
|
|
46
|
+
padding: 0 8px;
|
|
47
|
+
border: 0;
|
|
48
|
+
border-radius: var(--frock-radius-control);
|
|
49
|
+
color: var(--frock-text);
|
|
50
|
+
background: transparent;
|
|
51
|
+
font-size: var(--frock-text-md);
|
|
52
|
+
font-weight: 500;
|
|
53
|
+
text-align: left;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
transition: background-color var(--frock-motion-fast);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.package-entry-trigger:hover {
|
|
59
|
+
background: var(--frock-fill-hover);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.package-entry-trigger:active {
|
|
63
|
+
background: var(--frock-fill-pressed);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.package-entry-trigger__icon {
|
|
67
|
+
display: grid;
|
|
68
|
+
width: var(--frock-avatar-sm);
|
|
69
|
+
height: var(--frock-avatar-sm);
|
|
70
|
+
flex: 0 0 auto;
|
|
71
|
+
place-items: center;
|
|
72
|
+
border-radius: 8px;
|
|
73
|
+
color: var(--frock-action-primary);
|
|
74
|
+
background: var(--frock-surface);
|
|
75
|
+
box-shadow: inset 0 0 0 1px var(--frock-border);
|
|
76
|
+
}
|
|
77
|
+
</style>
|