Webhooks for the AT Protocol airglow.run
atproto atprotocol automation webhook
12
fork

Configure Feed

Select the types of activity you want to include in your feed.

ui: improve actions from and views

Hugo 63319fe8 d1b52b52

+587 -225
+43
app/components/ActionHeader/index.tsx
··· 1 + import type { Child } from "hono/jsx"; 2 + import { ACTION_INFO_BY_TYPE } from "../../../lib/automations/action-catalogue.js"; 3 + import { 4 + actionHeaderEyebrow, 5 + actionHeaderLabel, 6 + actionHeaderRow, 7 + actionHeaderSubtitle, 8 + actionIcon, 9 + } from "../../styles/action-header.css.ts"; 10 + 11 + type Props = { 12 + type: string; 13 + index: number; 14 + sameTypeIndex: number; 15 + totalOfType: number; 16 + as?: "h4" | "span"; 17 + children?: Child; 18 + }; 19 + 20 + export function ActionHeader({ 21 + type, 22 + index, 23 + sameTypeIndex, 24 + totalOfType, 25 + as: Tag = "span", 26 + children, 27 + }: Props) { 28 + const info = ACTION_INFO_BY_TYPE[type]; 29 + const Icon = info?.icon; 30 + return ( 31 + <Tag class={actionHeaderRow}> 32 + {Icon && ( 33 + <span class={actionIcon} data-cat={info.catId} aria-hidden="true"> 34 + <Icon size={18} /> 35 + </span> 36 + )} 37 + <span class={actionHeaderLabel}>{info?.label ?? type}</span> 38 + <span class={actionHeaderEyebrow}>Action {index + 1}</span> 39 + {totalOfType > 1 && <span class={actionHeaderSubtitle}>#{sameTypeIndex}</span>} 40 + {children} 41 + </Tag> 42 + ); 43 + }
+6
app/icons.ts
··· 38 38 import FilePlus2Data from "lucide/icons/file-plus-corner"; 39 39 import FilterData from "lucide/icons/funnel"; 40 40 import FlaskConicalData from "lucide/icons/flask-conical"; 41 + import HeartData from "lucide/icons/heart"; 42 + import MessageSquareData from "lucide/icons/message-square"; 41 43 import MoonData from "lucide/icons/moon"; 42 44 import PencilData from "lucide/icons/pencil"; 43 45 import PlusData from "lucide/icons/plus"; ··· 45 47 import RefreshCwData from "lucide/icons/refresh-cw"; 46 48 import SunData from "lucide/icons/sun"; 47 49 import Trash2Data from "lucide/icons/trash-2"; 50 + import UserPlusData from "lucide/icons/user-plus"; 48 51 import WebhookData from "lucide/icons/webhook"; 49 52 import ExternalLinkData from "lucide/icons/external-link"; 50 53 import GlobeData from "lucide/icons/globe"; ··· 63 66 export const FilePlus2 = icon(FilePlus2Data); 64 67 export const Filter = icon(FilterData); 65 68 export const FlaskConical = icon(FlaskConicalData); 69 + export const Heart = icon(HeartData); 70 + export const MessageSquare = icon(MessageSquareData); 66 71 export const Moon = icon(MoonData); 67 72 export const Pencil = icon(PencilData); 68 73 export const Plus = icon(PlusData); ··· 70 75 export const RefreshCw = icon(RefreshCwData); 71 76 export const Sun = icon(SunData); 72 77 export const Trash2 = icon(Trash2Data); 78 + export const UserPlus = icon(UserPlusData); 73 79 export const Webhook = icon(WebhookData); 74 80 export const Zap = icon(ZapData);
+114 -12
app/islands/AutomationForm.css.ts
··· 303 303 display: "flex", 304 304 justifyContent: "space-between", 305 305 alignItems: "center", 306 + gap: space[3], 306 307 }); 307 308 308 - export const actionTitle = style({ 309 + export const addActionsBox = style({ 310 + display: "flex", 311 + flexDirection: "column", 312 + gap: space[4], 313 + paddingBlock: space[4], 314 + paddingInline: space[4], 315 + borderRadius: radii.md, 316 + border: `1px dashed ${vars.color.border}`, 317 + backgroundColor: vars.color.bg, 318 + }); 319 + 320 + export const addActionsHeader = style({ 321 + margin: 0, 309 322 fontSize: fontSize.sm, 310 - fontWeight: fontWeight.medium, 311 - color: vars.color.text, 323 + color: vars.color.textSecondary, 324 + }); 325 + 326 + export const catGroup = style({ 327 + display: "flex", 328 + flexDirection: "column", 329 + gap: space[2], 330 + }); 331 + 332 + export const catGroupHeader = style({ 333 + display: "flex", 334 + alignItems: "center", 335 + gap: space[2], 336 + fontSize: fontSize.xs, 337 + textTransform: "uppercase", 338 + letterSpacing: "0.07em", 339 + fontWeight: fontWeight.semibold, 340 + color: vars.color.textMuted, 312 341 }); 313 342 314 - export const actionSubtitle = style({ 315 - fontFamily: "inherit", 343 + export const catGroupHeaderDesc = style({ 344 + color: vars.color.textMuted, 316 345 fontWeight: fontWeight.normal, 317 - color: vars.color.textMuted, 318 - marginInlineStart: space[2], 346 + textTransform: "none", 347 + letterSpacing: 0, 348 + }); 349 + 350 + export const catDot = style({ 351 + width: "8px", 352 + height: "8px", 353 + borderRadius: "50%", 354 + display: "inline-block", 355 + flexShrink: 0, 356 + selectors: { 357 + '&[data-cat="webhook"]': { backgroundColor: vars.color.accent }, 358 + '&[data-cat="bluesky"]': { backgroundColor: vars.color.bsky }, 359 + '&[data-cat="pds"]': { backgroundColor: vars.color.pds }, 360 + }, 319 361 }); 320 362 321 - export const addActionsRow = style({ 363 + export const catTileRow = style({ 364 + display: "grid", 365 + gridTemplateColumns: "repeat(3, minmax(0, 1fr))", 366 + gap: space[2], 367 + "@media": { 368 + "(max-width: 640px)": { 369 + gridTemplateColumns: "repeat(2, minmax(0, 1fr))", 370 + }, 371 + "(max-width: 400px)": { 372 + gridTemplateColumns: "1fr", 373 + }, 374 + }, 375 + }); 376 + 377 + export const catTile = style({ 322 378 display: "flex", 323 - flexWrap: "wrap", 379 + alignItems: "flex-start", 380 + gap: space[3], 381 + padding: space[3], 382 + backgroundColor: vars.color.surface, 383 + color: vars.color.text, 384 + border: `1px solid ${vars.color.border}`, 385 + borderRadius: radii.md, 386 + textAlign: "left", 387 + cursor: "pointer", 388 + fontFamily: "inherit", 389 + transition: "border-color 0.12s, background-color 0.12s", 390 + selectors: { 391 + "&:hover:not(:disabled)": { 392 + borderColor: vars.color.text, 393 + backgroundColor: vars.color.surfaceHover, 394 + }, 395 + "&:disabled": { 396 + opacity: 0.5, 397 + cursor: "not-allowed", 398 + }, 399 + }, 400 + }); 401 + 402 + export const catTileMeta = style({ 403 + display: "flex", 404 + flexDirection: "column", 405 + gap: "2px", 406 + minWidth: 0, 407 + flex: 1, 408 + }); 409 + 410 + export const catTileTitle = style({ 411 + fontSize: fontSize.base, 412 + fontWeight: fontWeight.medium, 413 + display: "flex", 414 + alignItems: "center", 324 415 gap: space[2], 325 416 }); 326 417 327 - export const addActionBtnDesc = style({ 328 - display: "block", 418 + export const catTileDesc = style({ 329 419 fontSize: fontSize.xs, 330 - fontWeight: fontWeight.normal, 420 + color: vars.color.textMuted, 421 + lineHeight: 1.35, 422 + }); 423 + 424 + export const comingSoonPill = style({ 425 + fontSize: "0.65rem", 426 + textTransform: "uppercase", 427 + letterSpacing: "0.06em", 428 + backgroundColor: vars.color.code, 331 429 color: vars.color.textMuted, 430 + paddingBlock: "2px", 431 + paddingInline: "6px", 432 + borderRadius: radii.sm, 433 + fontWeight: fontWeight.semibold, 332 434 }); 333 435 334 436 export const fetchRow = style({
+90 -66
app/islands/AutomationForm.tsx
··· 1 1 import { useState, useCallback, useRef, useMemo, useEffect } from "hono/jsx"; 2 2 import type { RecordSchema } from "../../lib/lexicons/schema-types.js"; 3 3 import { isRecordProducingAction, type Action, type FetchStep } from "../../lib/db/schema.js"; 4 - import { actionTypeLabels } from "../../lib/automations/labels.js"; 4 + import { ACTION_CATALOGUE, type AddableActionId } from "../../lib/automations/action-catalogue.js"; 5 5 import RecordFormBuilder from "./RecordFormBuilder.js"; 6 + import { ActionHeader } from "../components/ActionHeader/index.js"; 7 + import { actionIcon } from "../styles/action-header.css.ts"; 6 8 import * as s from "./AutomationForm.css.ts"; 7 9 8 10 type Field = { ··· 1283 1285 </p> 1284 1286 </div> 1285 1287 1286 - {actions.length === 0 && ( 1287 - <p class={s.hint}>Choose what happens when a matching event is detected.</p> 1288 - )} 1289 - 1290 - {actions.map((action, i) => ( 1291 - <div key={i} class={s.actionCard}> 1292 - <div class={s.actionHeader}> 1293 - <span class={s.actionTitle}> 1294 - Action {i + 1} 1295 - <span class={s.actionSubtitle}> 1296 - {actionTypeLabels[action.type] ?? action.type}{" "} 1297 - {actions.filter((a, j) => a.type === action.type && j <= i).length} 1298 - </span> 1299 - </span> 1300 - <button type="button" class={s.removeBtn} onClick={() => removeAction(i)}> 1301 - Remove 1302 - </button> 1303 - </div> 1304 - {action.type === "webhook" ? ( 1305 - <WebhookActionEditor action={action} onChange={(a) => updateAction(i, a)} /> 1306 - ) : action.type === "bsky-post" ? ( 1307 - <BskyPostActionEditor action={action} onChange={(a) => updateAction(i, a)} /> 1308 - ) : action.type === "patch-record" ? ( 1309 - <PatchRecordActionEditor 1310 - action={action} 1311 - onChange={(a) => updateAction(i, a)} 1312 - placeholders={allPlaceholders} 1313 - /> 1314 - ) : ( 1315 - <RecordActionEditor 1316 - action={action} 1317 - onChange={(a) => updateAction(i, a)} 1318 - placeholders={allPlaceholders} 1288 + {actions.map((action, i) => { 1289 + const sameTypeIndex = actions.filter( 1290 + (a, j) => a.type === action.type && j <= i, 1291 + ).length; 1292 + const totalOfType = actions.filter((a) => a.type === action.type).length; 1293 + return ( 1294 + <div key={i} class={s.actionCard}> 1295 + <div class={s.actionHeader}> 1296 + <ActionHeader 1297 + type={action.type} 1298 + index={i} 1299 + sameTypeIndex={sameTypeIndex} 1300 + totalOfType={totalOfType} 1301 + /> 1302 + <button type="button" class={s.removeBtn} onClick={() => removeAction(i)}> 1303 + Remove 1304 + </button> 1305 + </div> 1306 + {action.type === "webhook" ? ( 1307 + <WebhookActionEditor action={action} onChange={(a) => updateAction(i, a)} /> 1308 + ) : action.type === "bsky-post" ? ( 1309 + <BskyPostActionEditor action={action} onChange={(a) => updateAction(i, a)} /> 1310 + ) : action.type === "patch-record" ? ( 1311 + <PatchRecordActionEditor 1312 + action={action} 1313 + onChange={(a) => updateAction(i, a)} 1314 + placeholders={allPlaceholders} 1315 + /> 1316 + ) : ( 1317 + <RecordActionEditor 1318 + action={action} 1319 + onChange={(a) => updateAction(i, a)} 1320 + placeholders={allPlaceholders} 1321 + /> 1322 + )} 1323 + <input 1324 + class={s.input} 1325 + type="text" 1326 + placeholder="Note (optional)" 1327 + value={action.comment} 1328 + onInput={(e: Event) => 1329 + updateAction(i, { 1330 + ...action, 1331 + comment: (e.target as HTMLInputElement).value, 1332 + }) 1333 + } 1319 1334 /> 1320 - )} 1321 - <input 1322 - class={s.input} 1323 - type="text" 1324 - placeholder="Note (optional)" 1325 - value={action.comment} 1326 - onInput={(e: Event) => 1327 - updateAction(i, { 1328 - ...action, 1329 - comment: (e.target as HTMLInputElement).value, 1330 - }) 1331 - } 1332 - /> 1333 - </div> 1334 - ))} 1335 + </div> 1336 + ); 1337 + })} 1335 1338 1336 - <div class={s.addActionsRow}> 1337 - <button type="button" class={s.addBtn} onClick={() => addAction("webhook")}> 1338 - + Send a Webhook 1339 - <span class={s.addActionBtnDesc}>Send event data to an external URL</span> 1340 - </button> 1341 - <button type="button" class={s.addBtn} onClick={() => addAction("bsky-post")}> 1342 - + Post on Bluesky 1343 - <span class={s.addActionBtnDesc}>Publish a post to your Bluesky account</span> 1344 - </button> 1345 - <button type="button" class={s.addBtn} onClick={() => addAction("record")}> 1346 - + Create a Record 1347 - <span class={s.addActionBtnDesc}>Create a new record in any collection</span> 1348 - </button> 1349 - <button type="button" class={s.addBtn} onClick={() => addAction("patch-record")}> 1350 - + Update a Record 1351 - <span class={s.addActionBtnDesc}>Modify an existing record</span> 1352 - </button> 1339 + <div class={s.addActionsBox}> 1340 + <p class={s.addActionsHeader}> 1341 + {actions.length === 0 ? "Add an action" : "Add another action"} 1342 + </p> 1343 + {ACTION_CATALOGUE.map((cat) => ( 1344 + <div key={cat.id} class={s.catGroup}> 1345 + <div class={s.catGroupHeader}> 1346 + <span class={s.catDot} data-cat={cat.id} /> 1347 + {cat.label} 1348 + <span class={s.catGroupHeaderDesc}>— {cat.description}</span> 1349 + </div> 1350 + <div class={s.catTileRow}> 1351 + {cat.actions.map((a) => { 1352 + const Icon = a.icon; 1353 + return ( 1354 + <button 1355 + key={a.id} 1356 + type="button" 1357 + class={s.catTile} 1358 + disabled={!a.available} 1359 + onClick={() => addAction(a.id as AddableActionId)} 1360 + > 1361 + <span class={actionIcon} data-cat={cat.id} aria-hidden="true"> 1362 + <Icon size={20} /> 1363 + </span> 1364 + <span class={s.catTileMeta}> 1365 + <span class={s.catTileTitle}> 1366 + {a.label} 1367 + {!a.available && <span class={s.comingSoonPill}>Soon</span>} 1368 + </span> 1369 + <span class={s.catTileDesc}>{a.description}</span> 1370 + </span> 1371 + </button> 1372 + ); 1373 + })} 1374 + </div> 1375 + </div> 1376 + ))} 1353 1377 </div> 1354 1378 </div> 1355 1379
+86 -78
app/routes/dashboard/automations/[rkey].tsx
··· 20 20 import ThemeToggle from "../../../islands/ThemeToggle.js"; 21 21 import DeliveryLog from "../../../islands/DeliveryLog.js"; 22 22 import { inlineCluster, plainList, textMuted } from "../../../styles/utilities.css.js"; 23 + import { ActionHeader } from "../../../components/ActionHeader/index.js"; 24 + import { actionHeaderSubtitle } from "../../../styles/action-header.css.js"; 23 25 24 26 export default createRoute(async (c) => { 25 27 const user = c.get("user"); ··· 179 181 <h3 class={inlineCluster}> 180 182 <Zap size={18} /> Actions ({auto.actions.length}) 181 183 </h3> 182 - {auto.actions.map((action, i) => ( 183 - <Card key={i} variant="flat"> 184 - <Stack gap={3}> 185 - <h4> 186 - Action {i + 1}{" "} 187 - <span class={textMuted}> 188 - {actionTypeLabels[action.$type] ?? action.$type}{" "} 189 - {auto.actions.filter((a, j) => a.$type === action.$type && j <= i).length} 190 - </span> 191 - {action.$type === "webhook" && ( 192 - <> 193 - {" "} 184 + {auto.actions.map((action, i) => { 185 + const sameTypeIndex = auto.actions.filter( 186 + (a, j) => a.$type === action.$type && j <= i, 187 + ).length; 188 + const totalOfType = auto.actions.filter((a) => a.$type === action.$type).length; 189 + return ( 190 + <Card key={i} variant="flat"> 191 + <Stack gap={3}> 192 + <ActionHeader 193 + type={action.$type} 194 + index={i} 195 + sameTypeIndex={sameTypeIndex} 196 + totalOfType={totalOfType} 197 + as="h4" 198 + > 199 + {action.$type === "webhook" && ( 194 200 <Badge variant={action.verified ? "success" : "neutral"}> 195 201 {action.verified ? "Verified" : "Unverified"} 196 202 </Badge> 197 - </> 198 - )} 199 - {action.comment && <span class={textMuted}> — {action.comment}</span>} 200 - </h4> 201 - <DescriptionList> 202 - {action.$type === "webhook" ? ( 203 - <> 204 - <dt>Callback URL</dt> 205 - <dd> 206 - <InlineCode>{action.callbackUrl}</InlineCode> 207 - </dd> 208 - <dt>HMAC Secret</dt> 209 - <dd> 210 - <InlineCode>{action.secret}</InlineCode> 211 - </dd> 212 - </> 213 - ) : action.$type === "bsky-post" ? ( 214 - <> 215 - <dt>Text Template</dt> 216 - <dd> 217 - <CodeBlock>{action.textTemplate}</CodeBlock> 218 - </dd> 219 - {action.langs && action.langs.length > 0 && ( 220 - <> 221 - <dt>Languages</dt> 222 - <dd>{action.langs.join(", ")}</dd> 223 - </> 224 - )} 225 - {action.labels && action.labels.length > 0 && ( 226 - <> 227 - <dt>Content Warnings</dt> 228 - <dd>{action.labels.join(", ")}</dd> 229 - </> 230 - )} 231 - </> 232 - ) : action.$type === "patch-record" ? ( 233 - <> 234 - <dt>Target Collection</dt> 235 - <dd> 236 - <NsidCode>{action.targetCollection}</NsidCode> 237 - </dd> 238 - <dt>Base Record URI</dt> 239 - <dd> 240 - <InlineCode>{action.baseRecordUri}</InlineCode> 241 - </dd> 242 - <dt>Patch Template</dt> 243 - <dd> 244 - <CodeBlock>{action.recordTemplate}</CodeBlock> 245 - </dd> 246 - </> 247 - ) : ( 248 - <> 249 - <dt>Target Collection</dt> 250 - <dd> 251 - <NsidCode>{action.targetCollection}</NsidCode> 252 - </dd> 253 - <dt>Record Template</dt> 254 - <dd> 255 - <CodeBlock>{action.recordTemplate}</CodeBlock> 256 - </dd> 257 - </> 258 - )} 259 - </DescriptionList> 260 - </Stack> 261 - </Card> 262 - ))} 203 + )} 204 + {action.comment && ( 205 + <span class={actionHeaderSubtitle}>— {action.comment}</span> 206 + )} 207 + </ActionHeader> 208 + <DescriptionList> 209 + {action.$type === "webhook" ? ( 210 + <> 211 + <dt>Callback URL</dt> 212 + <dd> 213 + <InlineCode>{action.callbackUrl}</InlineCode> 214 + </dd> 215 + <dt>HMAC Secret</dt> 216 + <dd> 217 + <InlineCode>{action.secret}</InlineCode> 218 + </dd> 219 + </> 220 + ) : action.$type === "bsky-post" ? ( 221 + <> 222 + <dt>Text Template</dt> 223 + <dd> 224 + <CodeBlock>{action.textTemplate}</CodeBlock> 225 + </dd> 226 + {action.langs && action.langs.length > 0 && ( 227 + <> 228 + <dt>Languages</dt> 229 + <dd>{action.langs.join(", ")}</dd> 230 + </> 231 + )} 232 + {action.labels && action.labels.length > 0 && ( 233 + <> 234 + <dt>Content Warnings</dt> 235 + <dd>{action.labels.join(", ")}</dd> 236 + </> 237 + )} 238 + </> 239 + ) : action.$type === "patch-record" ? ( 240 + <> 241 + <dt>Target Collection</dt> 242 + <dd> 243 + <NsidCode>{action.targetCollection}</NsidCode> 244 + </dd> 245 + <dt>Base Record URI</dt> 246 + <dd> 247 + <InlineCode>{action.baseRecordUri}</InlineCode> 248 + </dd> 249 + <dt>Patch Template</dt> 250 + <dd> 251 + <CodeBlock>{action.recordTemplate}</CodeBlock> 252 + </dd> 253 + </> 254 + ) : ( 255 + <> 256 + <dt>Target Collection</dt> 257 + <dd> 258 + <NsidCode>{action.targetCollection}</NsidCode> 259 + </dd> 260 + <dt>Record Template</dt> 261 + <dd> 262 + <CodeBlock>{action.recordTemplate}</CodeBlock> 263 + </dd> 264 + </> 265 + )} 266 + </DescriptionList> 267 + </Stack> 268 + </Card> 269 + ); 270 + })} 263 271 </Stack> 264 272 265 273 <DeliveryLog
+77 -69
app/routes/u/[handle]/[rkey].tsx
··· 3 3 import { ArrowLeft, Copy, Filter, Database, Zap } from "../../../icons.js"; 4 4 import { getSessionUser } from "@/auth/middleware.js"; 5 5 import { resolveHandle } from "@/auth/client.js"; 6 - import { opLabels, actionTypeLabels, operationLabels } from "@/automations/labels.js"; 6 + import { opLabels, operationLabels } from "@/automations/labels.js"; 7 7 import { db } from "@/db/index.js"; 8 8 import { users, automations } from "@/db/schema.js"; 9 9 import { sanitizeActions } from "@/automations/sanitize.js"; ··· 25 25 textMuted, 26 26 centerTextSm, 27 27 } from "../../../styles/utilities.css.js"; 28 + import { ActionHeader } from "../../../components/ActionHeader/index.js"; 29 + import { actionHeaderSubtitle } from "../../../styles/action-header.css.js"; 28 30 29 31 export default createRoute(async (c) => { 30 32 const viewer = await getSessionUser(c); ··· 193 195 <h3 class={inlineCluster}> 194 196 <Zap size={18} /> Actions ({publicActions.length}) 195 197 </h3> 196 - {publicActions.map((action, i) => ( 197 - <Card key={i} variant="flat"> 198 - <Stack gap={3}> 199 - <h4> 200 - Action {i + 1}{" "} 201 - <span class={textMuted}> 202 - {actionTypeLabels[action.$type] ?? action.$type}{" "} 203 - {auto.actions.filter((a, j) => a.$type === action.$type && j <= i).length} 204 - </span> 205 - {action.$type === "webhook" && ( 206 - <> 207 - {" "} 198 + {publicActions.map((action, i) => { 199 + const sameTypeIndex = publicActions.filter( 200 + (a, j) => a.$type === action.$type && j <= i, 201 + ).length; 202 + const totalOfType = publicActions.filter((a) => a.$type === action.$type).length; 203 + return ( 204 + <Card key={i} variant="flat"> 205 + <Stack gap={3}> 206 + <ActionHeader 207 + type={action.$type} 208 + index={i} 209 + sameTypeIndex={sameTypeIndex} 210 + totalOfType={totalOfType} 211 + as="h4" 212 + > 213 + {action.$type === "webhook" && ( 208 214 <Badge variant={action.verified ? "success" : "neutral"}> 209 215 {action.verified ? "Verified" : "Unverified"} 210 216 </Badge> 211 - </> 212 - )} 213 - {action.comment && <span class={textMuted}> — {action.comment}</span>} 214 - </h4> 215 - <DescriptionList> 216 - {action.$type === "webhook" ? ( 217 - <> 218 - <dt>Destination</dt> 219 - <dd> 220 - <InlineCode>{action.callbackDomain}</InlineCode> 221 - </dd> 222 - </> 223 - ) : action.$type === "bsky-post" ? ( 224 - <> 225 - <dt>Text Template</dt> 226 - <dd> 227 - <CodeBlock>{action.textTemplate}</CodeBlock> 228 - </dd> 229 - {action.langs && action.langs.length > 0 && ( 230 - <> 231 - <dt>Languages</dt> 232 - <dd>{action.langs.join(", ")}</dd> 233 - </> 234 - )} 235 - </> 236 - ) : action.$type === "patch-record" ? ( 237 - <> 238 - <dt>Target Collection</dt> 239 - <dd> 240 - <NsidCode>{action.targetCollection}</NsidCode> 241 - </dd> 242 - <dt>Base Record URI</dt> 243 - <dd> 244 - <InlineCode>{action.baseRecordUri}</InlineCode> 245 - </dd> 246 - <dt>Patch Template</dt> 247 - <dd> 248 - <CodeBlock>{action.recordTemplate}</CodeBlock> 249 - </dd> 250 - </> 251 - ) : ( 252 - <> 253 - <dt>Target Collection</dt> 254 - <dd> 255 - <NsidCode>{action.targetCollection}</NsidCode> 256 - </dd> 257 - <dt>Record Template</dt> 258 - <dd> 259 - <CodeBlock>{action.recordTemplate}</CodeBlock> 260 - </dd> 261 - </> 262 - )} 263 - </DescriptionList> 264 - </Stack> 265 - </Card> 266 - ))} 217 + )} 218 + {action.comment && ( 219 + <span class={actionHeaderSubtitle}>— {action.comment}</span> 220 + )} 221 + </ActionHeader> 222 + <DescriptionList> 223 + {action.$type === "webhook" ? ( 224 + <> 225 + <dt>Destination</dt> 226 + <dd> 227 + <InlineCode>{action.callbackDomain}</InlineCode> 228 + </dd> 229 + </> 230 + ) : action.$type === "bsky-post" ? ( 231 + <> 232 + <dt>Text Template</dt> 233 + <dd> 234 + <CodeBlock>{action.textTemplate}</CodeBlock> 235 + </dd> 236 + {action.langs && action.langs.length > 0 && ( 237 + <> 238 + <dt>Languages</dt> 239 + <dd>{action.langs.join(", ")}</dd> 240 + </> 241 + )} 242 + </> 243 + ) : action.$type === "patch-record" ? ( 244 + <> 245 + <dt>Target Collection</dt> 246 + <dd> 247 + <NsidCode>{action.targetCollection}</NsidCode> 248 + </dd> 249 + <dt>Base Record URI</dt> 250 + <dd> 251 + <InlineCode>{action.baseRecordUri}</InlineCode> 252 + </dd> 253 + <dt>Patch Template</dt> 254 + <dd> 255 + <CodeBlock>{action.recordTemplate}</CodeBlock> 256 + </dd> 257 + </> 258 + ) : ( 259 + <> 260 + <dt>Target Collection</dt> 261 + <dd> 262 + <NsidCode>{action.targetCollection}</NsidCode> 263 + </dd> 264 + <dt>Record Template</dt> 265 + <dd> 266 + <CodeBlock>{action.recordTemplate}</CodeBlock> 267 + </dd> 268 + </> 269 + )} 270 + </DescriptionList> 271 + </Stack> 272 + </Card> 273 + ); 274 + })} 267 275 </Stack> 268 276 </Stack> 269 277 </Container>
+57
app/styles/action-header.css.ts
··· 1 + import { style } from "@vanilla-extract/css"; 2 + import { vars } from "./theme.css.ts"; 3 + import { radii } from "./tokens/radii.ts"; 4 + import { space } from "./tokens/spacing.ts"; 5 + import { fontSize, fontWeight } from "./tokens/typography.ts"; 6 + 7 + export const actionHeaderRow = style({ 8 + display: "flex", 9 + alignItems: "center", 10 + flexWrap: "wrap", 11 + gap: space[2], 12 + minWidth: 0, 13 + }); 14 + 15 + export const actionIcon = style({ 16 + display: "inline-flex", 17 + alignItems: "center", 18 + justifyContent: "center", 19 + width: "36px", 20 + height: "36px", 21 + borderRadius: radii.md, 22 + flexShrink: 0, 23 + selectors: { 24 + '&[data-cat="webhook"]': { 25 + backgroundColor: vars.color.accentSubtle, 26 + color: vars.color.accentActive, 27 + }, 28 + '&[data-cat="bluesky"]': { 29 + backgroundColor: vars.color.bskySubtle, 30 + color: vars.color.bsky, 31 + }, 32 + '&[data-cat="pds"]': { 33 + backgroundColor: vars.color.pdsSubtle, 34 + color: vars.color.pds, 35 + }, 36 + }, 37 + }); 38 + 39 + export const actionHeaderLabel = style({ 40 + fontSize: fontSize.base, 41 + fontWeight: fontWeight.medium, 42 + color: vars.color.text, 43 + }); 44 + 45 + export const actionHeaderEyebrow = style({ 46 + fontSize: fontSize.xs, 47 + fontWeight: fontWeight.semibold, 48 + textTransform: "uppercase", 49 + letterSpacing: "0.07em", 50 + color: vars.color.textMuted, 51 + }); 52 + 53 + export const actionHeaderSubtitle = style({ 54 + fontSize: fontSize.sm, 55 + fontWeight: fontWeight.normal, 56 + color: vars.color.textMuted, 57 + });
+8
app/styles/theme.css.ts
··· 28 28 warningSubtle: "color-warning-subtle", 29 29 error: "color-error", 30 30 errorSubtle: "color-error-subtle", 31 + bsky: "color-bsky", 32 + bskySubtle: "color-bsky-subtle", 33 + pds: "color-pds", 34 + pdsSubtle: "color-pds-subtle", 31 35 code: "color-code", 32 36 }, 33 37 shadow: { ··· 88 92 [vars.color.warningSubtle]: darkColors.warningSubtle, 89 93 [vars.color.error]: darkColors.error, 90 94 [vars.color.errorSubtle]: darkColors.errorSubtle, 95 + [vars.color.bsky]: darkColors.bsky, 96 + [vars.color.bskySubtle]: darkColors.bskySubtle, 97 + [vars.color.pds]: darkColors.pds, 98 + [vars.color.pdsSubtle]: darkColors.pdsSubtle, 91 99 [vars.color.code]: darkColors.code, 92 100 [vars.shadow.highlight]: darkShadows.highlight, 93 101 [vars.shadow.sm]: darkShadows.sm,
+10
app/styles/tokens/colors.ts
··· 28 28 error: "oklch(0.70 0.19 25)", 29 29 errorSubtle: "oklch(0.22 0.04 25)", 30 30 31 + bsky: "oklch(0.72 0.17 240)", 32 + bskySubtle: "oklch(0.22 0.05 240)", 33 + pds: "oklch(0.72 0.15 300)", 34 + pdsSubtle: "oklch(0.22 0.04 300)", 35 + 31 36 code: "oklch(0.20 0 0)", 32 37 } as const; 33 38 ··· 58 63 warningSubtle: "oklch(0.96 0.04 85)", 59 64 error: "oklch(0.55 0.22 25)", 60 65 errorSubtle: "oklch(0.96 0.04 25)", 66 + 67 + bsky: "oklch(0.58 0.18 240)", 68 + bskySubtle: "oklch(0.95 0.04 240)", 69 + pds: "oklch(0.55 0.16 300)", 70 + pdsSubtle: "oklch(0.96 0.03 300)", 61 71 62 72 code: "oklch(0.95 0.005 90)", 63 73 } as const;
+96
lib/automations/action-catalogue.ts
··· 1 + import { 2 + FilePlus2, 3 + Heart, 4 + MessageSquare, 5 + Pencil, 6 + Trash2, 7 + UserPlus, 8 + Webhook, 9 + } from "../../app/icons.js"; 10 + 11 + export type AddableActionId = "webhook" | "bsky-post" | "record" | "patch-record"; 12 + 13 + type ActionInfo = { 14 + label: string; 15 + icon: (typeof ACTION_CATALOGUE)[number]["actions"][number]["icon"]; 16 + catId: (typeof ACTION_CATALOGUE)[number]["id"]; 17 + }; 18 + 19 + export const ACTION_CATALOGUE = [ 20 + { 21 + id: "webhook", 22 + label: "Webhooks", 23 + description: "Send event data to your own server", 24 + actions: [ 25 + { 26 + id: "webhook", 27 + label: "Send a webhook", 28 + description: "POST event data to an external URL", 29 + icon: Webhook, 30 + available: true, 31 + }, 32 + ], 33 + }, 34 + { 35 + id: "bluesky", 36 + label: "Bluesky", 37 + description: "High-level Bluesky interactions", 38 + actions: [ 39 + { 40 + id: "bsky-post", 41 + label: "Post to Bluesky", 42 + description: "Publish a post to your Bluesky account", 43 + icon: MessageSquare, 44 + available: true, 45 + }, 46 + { 47 + id: "bsky-like", 48 + label: "Like a post", 49 + description: "Like a Bluesky post on your behalf", 50 + icon: Heart, 51 + available: false, 52 + }, 53 + { 54 + id: "bsky-follow", 55 + label: "Follow an account", 56 + description: "Follow another Bluesky user", 57 + icon: UserPlus, 58 + available: false, 59 + }, 60 + ], 61 + }, 62 + { 63 + id: "pds", 64 + label: "PDS records", 65 + description: "Low-level lexicon record operations", 66 + actions: [ 67 + { 68 + id: "record", 69 + label: "Create a record", 70 + description: "Create a new record in any collection", 71 + icon: FilePlus2, 72 + available: true, 73 + }, 74 + { 75 + id: "patch-record", 76 + label: "Update a record", 77 + description: "Modify fields of an existing record", 78 + icon: Pencil, 79 + available: true, 80 + }, 81 + { 82 + id: "delete-record", 83 + label: "Delete a record", 84 + description: "Remove a record from a collection", 85 + icon: Trash2, 86 + available: false, 87 + }, 88 + ], 89 + }, 90 + ]; 91 + 92 + export const ACTION_INFO_BY_TYPE: Record<string, ActionInfo> = Object.fromEntries( 93 + ACTION_CATALOGUE.flatMap((cat) => 94 + cat.actions.map((a) => [a.id, { label: a.label, icon: a.icon, catId: cat.id }] as const), 95 + ), 96 + );