@frockbot/plugin-routines 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/frockbot.json +39 -0
- package/package.json +53 -6
- package/src/agent.test.ts +206 -0
- package/src/agent.ts +345 -0
- package/src/backend.test.ts +181 -0
- package/src/backend.ts +375 -0
- package/src/client/RoutineInboxBadge.vue +216 -0
- package/src/client/RoutinesSection.vue +601 -0
- package/src/client/RoutinesSummary.vue +150 -0
- package/src/client/index.test.ts +209 -0
- package/src/client/index.ts +289 -0
- package/src/client/state.ts +65 -0
- package/src/cron.test.ts +217 -0
- package/src/cron.ts +246 -0
- package/src/env.d.ts +6 -0
- package/src/firing.ts +222 -0
- package/src/hook.test.ts +394 -0
- package/src/hook.ts +405 -0
- package/src/inbox-store.ts +405 -0
- package/src/inbox.test.ts +402 -0
- package/src/inbox.ts +405 -0
- package/src/index.ts +9 -0
- package/src/manifest.ts +3 -0
- package/src/records.test.ts +138 -0
- package/src/records.ts +341 -0
- package/src/scheduler.test.ts +482 -0
- package/src/scheduler.ts +551 -0
- package/src/shared.test.ts +141 -0
- package/src/shared.ts +988 -0
- package/src/storage-keys.ts +202 -0
- package/src/store.test.ts +261 -0
- package/src/store.ts +789 -0
- package/src/testing.ts +55 -0
- package/tsconfig.json +15 -0
- package/vite.config.ts +31 -0
- package/README.md +0 -3
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The Bot's Routines: the list, the create/edit form, and one run log per
|
|
3
|
+
// Routine. It renders durable state and submits versioned commands; it decides
|
|
4
|
+
// nothing — "Next run" is the moment the scheduler has actually armed an alarm
|
|
5
|
+
// on, sent down with the Routine, and blank when there is none to promise.
|
|
6
|
+
import { UiAnchor, UiButton, UiField, UiIcon } from "@frockbot/client-ui";
|
|
7
|
+
import { frockBotWebDataKey } from "@frockbot/plugin-shell/shared";
|
|
8
|
+
import { settingsLinkV1 } from "@frockbot/plugin-shell/settings-links";
|
|
9
|
+
import { computed, inject, reactive, ref, watch } from "vue";
|
|
10
|
+
import type { RoutineViewV1 } from "../shared.js";
|
|
11
|
+
import { routinesStateKey } from "./state.js";
|
|
12
|
+
|
|
13
|
+
const providedWeb = inject(frockBotWebDataKey);
|
|
14
|
+
const providedState = inject(routinesStateKey);
|
|
15
|
+
if (!providedWeb || !providedState) {
|
|
16
|
+
throw new Error("Routines client services were not provided");
|
|
17
|
+
}
|
|
18
|
+
const web = providedWeb;
|
|
19
|
+
const routines = providedState;
|
|
20
|
+
|
|
21
|
+
const botId = computed(() => web.value.activeBotId);
|
|
22
|
+
// The section is deep-linkable: an error message or a send payload may cite it.
|
|
23
|
+
const anchorHref = computed(() =>
|
|
24
|
+
settingsLinkV1({ anchor: "bot-routines", botId: botId.value }),
|
|
25
|
+
);
|
|
26
|
+
const formOpen = ref(false);
|
|
27
|
+
const openLog = ref<string>();
|
|
28
|
+
const copied = ref(false);
|
|
29
|
+
const openRun = ref<string>();
|
|
30
|
+
|
|
31
|
+
// An automation Turn never appears in the transcript, so opening a run here is
|
|
32
|
+
// the only way to read one, and it is read-only in both directions: the view
|
|
33
|
+
// carries what happened and no way to act on it.
|
|
34
|
+
function toggleRun(routineId: string, runId: string): void {
|
|
35
|
+
if (openRun.value === runId) {
|
|
36
|
+
openRun.value = undefined;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
openRun.value = runId;
|
|
40
|
+
if (botId.value && !routines.value.runDetails[runId]) {
|
|
41
|
+
void routines.value.loadRun(botId.value, routineId, runId);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const form = reactive({
|
|
45
|
+
routineId: undefined as string | undefined,
|
|
46
|
+
name: "",
|
|
47
|
+
prompt: "",
|
|
48
|
+
timing: "schedule" as "schedule" | "webhook",
|
|
49
|
+
schedule: "",
|
|
50
|
+
timezone: "UTC",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
watch(
|
|
54
|
+
botId,
|
|
55
|
+
(id) => {
|
|
56
|
+
if (!id) return;
|
|
57
|
+
if (routines.value.botId !== id || !routines.value.loaded) {
|
|
58
|
+
void routines.value.load(id);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{ immediate: true },
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The delivery URL, composed here because the browser is the only place that
|
|
66
|
+
* knows the origin the caller will use. The key is appended as the bearer token
|
|
67
|
+
* the route reads, and it is on screen once: nothing can fetch it again.
|
|
68
|
+
*/
|
|
69
|
+
function hookUrl(): string {
|
|
70
|
+
const mint = routines.value.mintedHook;
|
|
71
|
+
if (!mint) return "";
|
|
72
|
+
return `${window.location.origin}${mint.path}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function copyHook(): Promise<void> {
|
|
76
|
+
const mint = routines.value.mintedHook;
|
|
77
|
+
if (!mint) return;
|
|
78
|
+
try {
|
|
79
|
+
await navigator.clipboard.writeText(
|
|
80
|
+
`curl -X POST ${hookUrl()} -H "Authorization: Bearer ${mint.token}" -d '{}'`,
|
|
81
|
+
);
|
|
82
|
+
copied.value = true;
|
|
83
|
+
} catch {
|
|
84
|
+
// Clipboard access can be refused; the key is on screen either way.
|
|
85
|
+
copied.value = false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function summary(routine: RoutineViewV1): string {
|
|
90
|
+
return routine.schedule
|
|
91
|
+
? `${routine.schedule} · ${routine.timezone}`
|
|
92
|
+
: "Webhook trigger";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function startCreate(): void {
|
|
96
|
+
form.routineId = undefined;
|
|
97
|
+
form.name = "";
|
|
98
|
+
form.prompt = "";
|
|
99
|
+
form.timing = "schedule";
|
|
100
|
+
form.schedule = "0 9 * * *";
|
|
101
|
+
form.timezone = "UTC";
|
|
102
|
+
formOpen.value = true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function startEdit(routine: RoutineViewV1): void {
|
|
106
|
+
form.routineId = routine.routineId;
|
|
107
|
+
form.name = routine.name;
|
|
108
|
+
form.prompt = routine.prompt;
|
|
109
|
+
form.timing = routine.schedule ? "schedule" : "webhook";
|
|
110
|
+
form.schedule = routine.schedule ?? "";
|
|
111
|
+
form.timezone = routine.timezone;
|
|
112
|
+
formOpen.value = true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function submit(): Promise<void> {
|
|
116
|
+
const id = botId.value;
|
|
117
|
+
if (!id) return;
|
|
118
|
+
try {
|
|
119
|
+
await routines.value.save(id, {
|
|
120
|
+
...(form.routineId ? { routineId: form.routineId } : {}),
|
|
121
|
+
name: form.name.trim(),
|
|
122
|
+
prompt: form.prompt.trim(),
|
|
123
|
+
...(form.timing === "schedule"
|
|
124
|
+
? { schedule: form.schedule.trim() }
|
|
125
|
+
: { trigger: { kind: "webhook" as const } }),
|
|
126
|
+
timezone: form.timezone.trim(),
|
|
127
|
+
});
|
|
128
|
+
formOpen.value = false;
|
|
129
|
+
} catch {
|
|
130
|
+
// The state holds the reason; the form stays open so it can be corrected.
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function toggleLog(routineId: string): Promise<void> {
|
|
135
|
+
const id = botId.value;
|
|
136
|
+
if (!id) return;
|
|
137
|
+
if (openLog.value === routineId) {
|
|
138
|
+
openLog.value = undefined;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
openLog.value = routineId;
|
|
142
|
+
await routines.value.loadRuns(id, routineId);
|
|
143
|
+
}
|
|
144
|
+
</script>
|
|
145
|
+
|
|
146
|
+
<template>
|
|
147
|
+
<UiAnchor
|
|
148
|
+
as="section"
|
|
149
|
+
anchor="bot-routines"
|
|
150
|
+
label="Routines"
|
|
151
|
+
:href="anchorHref"
|
|
152
|
+
class="routines"
|
|
153
|
+
>
|
|
154
|
+
<header class="routines__header">
|
|
155
|
+
<span class="routines__icon" aria-hidden="true"
|
|
156
|
+
><UiIcon name="history"
|
|
157
|
+
/></span>
|
|
158
|
+
<span class="routines__intro">
|
|
159
|
+
<strong>Routines</strong>
|
|
160
|
+
<small>
|
|
161
|
+
Standing instructions this Bot runs on a schedule or on a delivered
|
|
162
|
+
webhook, as their own Turns.
|
|
163
|
+
</small>
|
|
164
|
+
</span>
|
|
165
|
+
<UiButton type="button" :disabled="!botId" @click="startCreate">
|
|
166
|
+
New Routine
|
|
167
|
+
</UiButton>
|
|
168
|
+
</header>
|
|
169
|
+
|
|
170
|
+
<p v-if="routines.error" class="routines__error" role="alert">
|
|
171
|
+
{{ routines.error }}
|
|
172
|
+
</p>
|
|
173
|
+
|
|
174
|
+
<p
|
|
175
|
+
v-if="routines.loaded && routines.routines.length === 0"
|
|
176
|
+
class="routines__empty"
|
|
177
|
+
>
|
|
178
|
+
No Routines yet. A Routine fires on its own and reports back on this Bot's
|
|
179
|
+
next conversation.
|
|
180
|
+
</p>
|
|
181
|
+
|
|
182
|
+
<article
|
|
183
|
+
v-for="routine in routines.routines"
|
|
184
|
+
:key="routine.routineId"
|
|
185
|
+
class="routine-card"
|
|
186
|
+
>
|
|
187
|
+
<div class="routine-card__head">
|
|
188
|
+
<span class="routine-card__text">
|
|
189
|
+
<strong>{{ routine.name }}</strong>
|
|
190
|
+
<small>{{ summary(routine) }}</small>
|
|
191
|
+
</span>
|
|
192
|
+
<span
|
|
193
|
+
class="routine-card__state"
|
|
194
|
+
:data-enabled="routine.enabled ? 'yes' : 'no'"
|
|
195
|
+
>
|
|
196
|
+
{{ routine.enabled ? "Enabled" : "Paused" }}
|
|
197
|
+
</span>
|
|
198
|
+
</div>
|
|
199
|
+
<div
|
|
200
|
+
v-if="routines.mintedHook?.routineId === routine.routineId"
|
|
201
|
+
class="routine-hook"
|
|
202
|
+
>
|
|
203
|
+
<strong
|
|
204
|
+
>Webhook key, version {{ routines.mintedHook.keyVersion }}</strong
|
|
205
|
+
>
|
|
206
|
+
<small>
|
|
207
|
+
This is the only time it is shown. Copy it now; rotating is the only
|
|
208
|
+
way to see a key again.
|
|
209
|
+
</small>
|
|
210
|
+
<code>{{ hookUrl() }}</code>
|
|
211
|
+
<code class="routine-hook__token">{{ routines.mintedHook.token }}</code>
|
|
212
|
+
<div class="routine-card__actions">
|
|
213
|
+
<UiButton type="button" variant="primary" @click="copyHook">
|
|
214
|
+
{{ copied ? "Copied" : "Copy webhook URL" }}
|
|
215
|
+
</UiButton>
|
|
216
|
+
<UiButton type="button" @click="routines.dismissHook()">
|
|
217
|
+
Done
|
|
218
|
+
</UiButton>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
<dl class="routine-card__facts">
|
|
222
|
+
<div>
|
|
223
|
+
<dt>Next run</dt>
|
|
224
|
+
<dd>{{ routine.nextRunAt ?? "—" }}</dd>
|
|
225
|
+
</div>
|
|
226
|
+
<div v-if="routine.trigger">
|
|
227
|
+
<dt>Webhook key</dt>
|
|
228
|
+
<dd>
|
|
229
|
+
{{
|
|
230
|
+
routine.hookKeyVersion
|
|
231
|
+
? `version ${routine.hookKeyVersion}`
|
|
232
|
+
: "none"
|
|
233
|
+
}}
|
|
234
|
+
</dd>
|
|
235
|
+
</div>
|
|
236
|
+
<div>
|
|
237
|
+
<dt>Last run</dt>
|
|
238
|
+
<dd>{{ routine.lastRunAt ?? "Never" }}</dd>
|
|
239
|
+
</div>
|
|
240
|
+
<div>
|
|
241
|
+
<dt>Written by</dt>
|
|
242
|
+
<dd>{{ routine.updatedBy.kind === "bot" ? "the Bot" : "you" }}</dd>
|
|
243
|
+
</div>
|
|
244
|
+
</dl>
|
|
245
|
+
<div class="routine-card__actions">
|
|
246
|
+
<UiButton
|
|
247
|
+
type="button"
|
|
248
|
+
:disabled="routines.busy"
|
|
249
|
+
@click="startEdit(routine)"
|
|
250
|
+
>
|
|
251
|
+
Edit
|
|
252
|
+
</UiButton>
|
|
253
|
+
<UiButton
|
|
254
|
+
type="button"
|
|
255
|
+
:disabled="routines.busy"
|
|
256
|
+
@click="
|
|
257
|
+
botId &&
|
|
258
|
+
routines.setEnabled(botId, routine.routineId, !routine.enabled)
|
|
259
|
+
"
|
|
260
|
+
>
|
|
261
|
+
{{ routine.enabled ? "Pause" : "Resume" }}
|
|
262
|
+
</UiButton>
|
|
263
|
+
<UiButton
|
|
264
|
+
type="button"
|
|
265
|
+
:disabled="routines.busy"
|
|
266
|
+
@click="botId && routines.runNow(botId, routine.routineId)"
|
|
267
|
+
>
|
|
268
|
+
Run now
|
|
269
|
+
</UiButton>
|
|
270
|
+
<UiButton
|
|
271
|
+
v-if="routine.trigger"
|
|
272
|
+
type="button"
|
|
273
|
+
:disabled="routines.busy"
|
|
274
|
+
@click="botId && routines.rotateKey(botId, routine.routineId)"
|
|
275
|
+
>
|
|
276
|
+
{{ routine.hookKeyVersion ? "Rotate key" : "Mint key" }}
|
|
277
|
+
</UiButton>
|
|
278
|
+
<UiButton
|
|
279
|
+
v-if="routine.trigger && routine.hookKeyVersion"
|
|
280
|
+
type="button"
|
|
281
|
+
:disabled="routines.busy"
|
|
282
|
+
@click="botId && routines.revokeKey(botId, routine.routineId)"
|
|
283
|
+
>
|
|
284
|
+
Revoke key
|
|
285
|
+
</UiButton>
|
|
286
|
+
<UiButton type="button" @click="toggleLog(routine.routineId)">
|
|
287
|
+
{{ openLog === routine.routineId ? "Hide runs" : "Run log" }}
|
|
288
|
+
</UiButton>
|
|
289
|
+
<UiButton
|
|
290
|
+
type="button"
|
|
291
|
+
variant="danger"
|
|
292
|
+
:disabled="routines.busy"
|
|
293
|
+
@click="botId && routines.remove(botId, routine.routineId)"
|
|
294
|
+
>
|
|
295
|
+
Delete
|
|
296
|
+
</UiButton>
|
|
297
|
+
</div>
|
|
298
|
+
<div v-if="openLog === routine.routineId" class="routine-card__log">
|
|
299
|
+
<p
|
|
300
|
+
v-if="(routines.runs[routine.routineId] ?? []).length === 0"
|
|
301
|
+
class="routines__empty"
|
|
302
|
+
>
|
|
303
|
+
This Routine has not fired yet.
|
|
304
|
+
</p>
|
|
305
|
+
<ul v-else>
|
|
306
|
+
<li
|
|
307
|
+
v-for="entry in routines.runs[routine.routineId]"
|
|
308
|
+
:key="entry.entryId"
|
|
309
|
+
>
|
|
310
|
+
<button
|
|
311
|
+
type="button"
|
|
312
|
+
class="routine-card__run"
|
|
313
|
+
:aria-expanded="openRun === entry.runId"
|
|
314
|
+
@click="toggleRun(routine.routineId, entry.runId)"
|
|
315
|
+
>
|
|
316
|
+
<span>{{ entry.startedAt }}</span>
|
|
317
|
+
<span>{{ entry.trigger }}</span>
|
|
318
|
+
<span>{{ entry.status }}</span>
|
|
319
|
+
</button>
|
|
320
|
+
<ol
|
|
321
|
+
v-if="openRun === entry.runId && routines.runDetails[entry.runId]"
|
|
322
|
+
class="routine-card__events"
|
|
323
|
+
>
|
|
324
|
+
<li
|
|
325
|
+
v-for="(event, index) in routines.runDetails[entry.runId]!
|
|
326
|
+
.events"
|
|
327
|
+
:key="`${entry.runId}:${index}`"
|
|
328
|
+
>
|
|
329
|
+
{{ event.summary }}
|
|
330
|
+
</li>
|
|
331
|
+
</ol>
|
|
332
|
+
</li>
|
|
333
|
+
</ul>
|
|
334
|
+
</div>
|
|
335
|
+
</article>
|
|
336
|
+
|
|
337
|
+
<div v-if="formOpen" class="routine-form">
|
|
338
|
+
<strong>{{ form.routineId ? "Edit Routine" : "New Routine" }}</strong>
|
|
339
|
+
<UiField label="Name">
|
|
340
|
+
<input v-model="form.name" maxlength="100" required />
|
|
341
|
+
</UiField>
|
|
342
|
+
<UiField label="Prompt" hint="what the Routine does when it fires">
|
|
343
|
+
<textarea v-model="form.prompt" maxlength="8000" rows="5" />
|
|
344
|
+
</UiField>
|
|
345
|
+
<fieldset class="routine-form__timing">
|
|
346
|
+
<legend>Fires on</legend>
|
|
347
|
+
<label>
|
|
348
|
+
<input v-model="form.timing" type="radio" value="schedule" />
|
|
349
|
+
<span>A schedule</span>
|
|
350
|
+
</label>
|
|
351
|
+
<label>
|
|
352
|
+
<input v-model="form.timing" type="radio" value="webhook" />
|
|
353
|
+
<span>A webhook</span>
|
|
354
|
+
</label>
|
|
355
|
+
</fieldset>
|
|
356
|
+
<UiField
|
|
357
|
+
v-if="form.timing === 'schedule'"
|
|
358
|
+
label="Schedule"
|
|
359
|
+
hint="cron, or @daily / @every 15m"
|
|
360
|
+
>
|
|
361
|
+
<input
|
|
362
|
+
v-model="form.schedule"
|
|
363
|
+
maxlength="256"
|
|
364
|
+
placeholder="0 9 * * *"
|
|
365
|
+
/>
|
|
366
|
+
</UiField>
|
|
367
|
+
<p v-else class="routines__note">
|
|
368
|
+
A delivery key is minted when the Routine is saved, and shown once.
|
|
369
|
+
</p>
|
|
370
|
+
<UiField label="Time zone">
|
|
371
|
+
<input
|
|
372
|
+
v-model="form.timezone"
|
|
373
|
+
maxlength="64"
|
|
374
|
+
placeholder="Australia/Sydney"
|
|
375
|
+
/>
|
|
376
|
+
</UiField>
|
|
377
|
+
<div class="routine-card__actions">
|
|
378
|
+
<UiButton
|
|
379
|
+
type="button"
|
|
380
|
+
variant="primary"
|
|
381
|
+
:disabled="routines.busy"
|
|
382
|
+
@click="submit"
|
|
383
|
+
>
|
|
384
|
+
{{ routines.busy ? "Saving…" : "Save Routine" }}
|
|
385
|
+
</UiButton>
|
|
386
|
+
<UiButton type="button" @click="formOpen = false">Cancel</UiButton>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
</UiAnchor>
|
|
390
|
+
</template>
|
|
391
|
+
|
|
392
|
+
<style scoped>
|
|
393
|
+
.routines {
|
|
394
|
+
display: flex;
|
|
395
|
+
flex-direction: column;
|
|
396
|
+
gap: 12px;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.routines__header {
|
|
400
|
+
display: flex;
|
|
401
|
+
align-items: center;
|
|
402
|
+
gap: 10px;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.routines__icon {
|
|
406
|
+
display: grid;
|
|
407
|
+
width: var(--frock-avatar-sm);
|
|
408
|
+
height: var(--frock-avatar-sm);
|
|
409
|
+
flex: 0 0 auto;
|
|
410
|
+
place-items: center;
|
|
411
|
+
border-radius: 8px;
|
|
412
|
+
color: var(--frock-action-primary);
|
|
413
|
+
background: var(--frock-surface);
|
|
414
|
+
box-shadow: inset 0 0 0 1px var(--frock-border);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.routines__intro {
|
|
418
|
+
display: flex;
|
|
419
|
+
min-width: 0;
|
|
420
|
+
flex: 1 1 auto;
|
|
421
|
+
flex-direction: column;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.routines__intro strong {
|
|
425
|
+
color: var(--frock-text);
|
|
426
|
+
font-size: var(--frock-text-md);
|
|
427
|
+
font-weight: 600;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.routines__intro small,
|
|
431
|
+
.routines__empty,
|
|
432
|
+
.routines__note {
|
|
433
|
+
color: var(--frock-text-muted);
|
|
434
|
+
font-size: var(--frock-text-sm);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.routines__error {
|
|
438
|
+
color: var(--frock-danger-text);
|
|
439
|
+
font-size: var(--frock-text-sm);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.routine-card,
|
|
443
|
+
.routine-form {
|
|
444
|
+
display: flex;
|
|
445
|
+
flex-direction: column;
|
|
446
|
+
gap: 10px;
|
|
447
|
+
border: 1px solid var(--frock-border);
|
|
448
|
+
border-radius: var(--frock-radius-card);
|
|
449
|
+
padding: 12px;
|
|
450
|
+
background: var(--frock-surface-subtle);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.routine-card__head {
|
|
454
|
+
display: flex;
|
|
455
|
+
align-items: center;
|
|
456
|
+
gap: 10px;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.routine-card__text {
|
|
460
|
+
display: flex;
|
|
461
|
+
min-width: 0;
|
|
462
|
+
flex: 1 1 auto;
|
|
463
|
+
flex-direction: column;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.routine-card__text strong {
|
|
467
|
+
color: var(--frock-text);
|
|
468
|
+
font-size: var(--frock-text-md);
|
|
469
|
+
font-weight: 600;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.routine-card__text small {
|
|
473
|
+
color: var(--frock-text-muted);
|
|
474
|
+
font-size: var(--frock-text-sm);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
.routine-card__state {
|
|
478
|
+
flex: 0 0 auto;
|
|
479
|
+
border-radius: var(--frock-radius-control);
|
|
480
|
+
padding: 2px 8px;
|
|
481
|
+
color: var(--frock-text-muted);
|
|
482
|
+
font-size: var(--frock-text-sm);
|
|
483
|
+
box-shadow: inset 0 0 0 1px var(--frock-border);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.routine-card__state[data-enabled="yes"] {
|
|
487
|
+
color: var(--frock-action-primary);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.routine-card__facts {
|
|
491
|
+
display: flex;
|
|
492
|
+
flex-wrap: wrap;
|
|
493
|
+
gap: 16px;
|
|
494
|
+
margin: 0;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.routine-card__facts dt {
|
|
498
|
+
color: var(--frock-text-subtle);
|
|
499
|
+
font-size: var(--frock-text-sm);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.routine-card__facts dd {
|
|
503
|
+
margin: 0;
|
|
504
|
+
color: var(--frock-text);
|
|
505
|
+
font-size: var(--frock-text-sm);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.routine-hook {
|
|
509
|
+
display: flex;
|
|
510
|
+
flex-direction: column;
|
|
511
|
+
gap: 6px;
|
|
512
|
+
border: 1px solid var(--frock-border);
|
|
513
|
+
border-radius: var(--frock-radius-card);
|
|
514
|
+
padding: 10px;
|
|
515
|
+
background: var(--frock-surface);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.routine-hook strong {
|
|
519
|
+
color: var(--frock-text);
|
|
520
|
+
font-size: var(--frock-text-sm);
|
|
521
|
+
font-weight: 600;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.routine-hook code {
|
|
525
|
+
overflow-wrap: anywhere;
|
|
526
|
+
color: var(--frock-text);
|
|
527
|
+
font-size: var(--frock-text-sm);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.routine-hook__token {
|
|
531
|
+
color: var(--frock-text-muted);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.routine-card__actions {
|
|
535
|
+
display: flex;
|
|
536
|
+
flex-wrap: wrap;
|
|
537
|
+
gap: 8px;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.routine-card__log ul {
|
|
541
|
+
display: flex;
|
|
542
|
+
flex-direction: column;
|
|
543
|
+
gap: 6px;
|
|
544
|
+
margin: 0;
|
|
545
|
+
padding: 0;
|
|
546
|
+
list-style: none;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.routine-card__log li {
|
|
550
|
+
display: flex;
|
|
551
|
+
flex-direction: column;
|
|
552
|
+
gap: 4px;
|
|
553
|
+
color: var(--frock-text-muted);
|
|
554
|
+
font-size: var(--frock-text-sm);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.routine-card__run {
|
|
558
|
+
display: flex;
|
|
559
|
+
justify-content: space-between;
|
|
560
|
+
gap: 8px;
|
|
561
|
+
border: 0;
|
|
562
|
+
padding: 0;
|
|
563
|
+
color: inherit;
|
|
564
|
+
background: transparent;
|
|
565
|
+
font: inherit;
|
|
566
|
+
text-align: left;
|
|
567
|
+
cursor: pointer;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
.routine-card__events {
|
|
571
|
+
display: flex;
|
|
572
|
+
flex-direction: column;
|
|
573
|
+
gap: 4px;
|
|
574
|
+
margin: 0;
|
|
575
|
+
padding: 6px 0 6px 16px;
|
|
576
|
+
color: var(--frock-text-subtle);
|
|
577
|
+
font-size: var(--frock-text-xs);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.routine-form__timing {
|
|
581
|
+
display: flex;
|
|
582
|
+
gap: 12px;
|
|
583
|
+
border: 0;
|
|
584
|
+
margin: 0;
|
|
585
|
+
padding: 0;
|
|
586
|
+
color: var(--frock-text-muted);
|
|
587
|
+
font-size: var(--frock-text-sm);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.routine-form__timing legend {
|
|
591
|
+
padding: 0;
|
|
592
|
+
color: var(--frock-text-subtle);
|
|
593
|
+
font-size: var(--frock-text-sm);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.routine-form__timing label {
|
|
597
|
+
display: flex;
|
|
598
|
+
align-items: center;
|
|
599
|
+
gap: 6px;
|
|
600
|
+
}
|
|
601
|
+
</style>
|