diff --git a/.github/ISSUE_TEMPLATE/bug-issue.md b/.github/ISSUE_TEMPLATE/bug-issue.md index 99b47063e0..10ec8651e1 100644 --- a/.github/ISSUE_TEMPLATE/bug-issue.md +++ b/.github/ISSUE_TEMPLATE/bug-issue.md @@ -1,10 +1,9 @@ --- name: Bug Report about: Find something broken or odd? Report it here. -title: '' +title: "" labels: bug -assignees: '' - +assignees: "" --- ### What went wrong, step-by-step? + 1. 2. ... diff --git a/.github/ISSUE_TEMPLATE/feature-issue.md b/.github/ISSUE_TEMPLATE/feature-issue.md index e23153e9c9..0556cda695 100644 --- a/.github/ISSUE_TEMPLATE/feature-issue.md +++ b/.github/ISSUE_TEMPLATE/feature-issue.md @@ -1,19 +1,21 @@ --- name: Feature Issue about: Suggest a new feature at a high level. -title: '' +title: "" labels: feature -assignees: '' - +assignees: "" --- ## Motivation + ## Requirements + ## Acceptance Criteria + diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index d66d99b1b3..8426f667ff 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -4,14 +4,14 @@ Docker-compose provides a means to run all the code components as containers. It has these advantages: -- more realistically emulating the setting where this code is run in production. -- less contamination of environment, so spurious failures (or successes) can be avoided. -- easy to boot from nothing without system dependencies except Docker +- more realistically emulating the setting where this code is run in production. +- less contamination of environment, so spurious failures (or successes) can be avoided. +- easy to boot from nothing without system dependencies except Docker With disadvantages: -- doesn't support hot-reloading -- slightly slower iteration due to `docker build` between runs +- doesn't support hot-reloading +- slightly slower iteration due to `docker build` between runs With these properties, this is useful for end-to-end tests and locally verifying that the code works when removed from some of the fast-iteration features of `next.js`, such as JIT compilation. Because a `docker build` is needed to build the containers to run, hot-reloading is not available in this environment; so faster iteration @@ -39,8 +39,8 @@ you can now address on `localhost:3000` as before. note that `pnpm dev` uses the Two hooks are defined using `husky` and stored in `.husky`. -- The first runs Prettier on commit -- The second runs a type-check before pushing. Since our deployment setup builds on each push, the intent here is to not trigger a build with known type errors. +- The first runs Prettier on commit +- The second runs a type-check before pushing. Since our deployment setup builds on each push, the intent here is to not trigger a build with known type errors. Sometimes you want to push up changes even though there is a type error. To do so, include `--no-verify` at the end of your command. For example: `git push origin main --no-verify`. @@ -48,8 +48,8 @@ Sometimes you want to push up changes even though there is a type error. To do s We currently have a race condition where dev will sometimes fail because we can't specify the order of dependency builds. Tied to the fact that we clean out the dist folder on build, but upstream packages are watching dist. -- https://github.com/vercel/turbo/discussions/1299?sort=top?sort=top -- https://github.com/vercel/turbo/issues/460 +- https://github.com/vercel/turbo/discussions/1299?sort=top?sort=top +- https://github.com/vercel/turbo/issues/460 `core` depends on `ui` which depends on `utils`. `utils` often takes longer to build than it does for `ui` to start building, which causes an error to be thrown because `utils` d.ts file has been cleared out during its build and hasn't been replaced yet. This generates an error, but is quick to resolve, so doesn't break actual dev work from beginning. It does make the console output messier though. @@ -137,4 +137,4 @@ Images tagged with a SHA alone should be idempotently built, but `-dirty` can be **TODO:** -- [ ] allow deploying without a rebuild, so that a rollback is convenient +- [ ] allow deploying without a rebuild, so that a rollback is convenient diff --git a/README.md b/README.md index e592448244..73b038c76b 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,10 @@ root └── ... ``` -- `core` holds the primary web application. -- `infrastructure` holds the deployment infrastructure for the centrally hosted version of PubPub Platform run by the Knowledge Futures team. -- `jobs` holds the job queueing and scheduling service used by `core`. -- `packages` holds libraries and npm packages that are shared by `core`, `jobs`, and `infrastructure`. +- `core` holds the primary web application. +- `infrastructure` holds the deployment infrastructure for the centrally hosted version of PubPub Platform run by the Knowledge Futures team. +- `jobs` holds the job queueing and scheduling service used by `core`. +- `packages` holds libraries and npm packages that are shared by `core`, `jobs`, and `infrastructure`. To avoid inconsistencies and difficult-to-track errors, we specify a particular version of node in `/.nvmrc` (currently `v22.13.1`). We recommend using [nvm](https://github.com/nvm-sh/nvm) to ensure you're using the same version. @@ -103,10 +103,10 @@ If you have a feature request, idea, general feedback, or need help with PubPub, In the coming weeks, we'll be developing more thorough contribution guides, particularly for contributors interested in: -- Extending PubPub Platform with new Actions and Rules -- Extending the PubPub Platform API -- Contributing to self-hosting scripts and guides on common cloud hosting -- Contributing documentation for developers or users +- Extending PubPub Platform with new Actions and Rules +- Extending the PubPub Platform API +- Contributing to self-hosting scripts and guides on common cloud hosting +- Contributing documentation for developers or users For now, you can browse the [issue list](https://github.com/pubpub/platform/issues) and comment on any issues you may want to take on. We'll be in touch shortly @@ -117,10 +117,10 @@ Our preferred practice is for contributors to create a branch using the format ` Request names should be prefixed with one of the following categories: -- fix: for commits focused on specific bug fixes -- feature: for commits that introduce a new feature -- update: for commits that improve an existing feature -- dev: for commits that focus solely on documentation, refactoring code, or developer experience updates +- fix: for commits focused on specific bug fixes +- feature: for commits that introduce a new feature +- update: for commits that improve an existing feature +- dev: for commits that focus solely on documentation, refactoring code, or developer experience updates Request descriptions should use to our Pull Request template, including a clear rationale for the PR, listing any issues resolved, and describing the test plan for the request, including both tests you wrote and step-by-step descriptions of any manual QA that may be needed. diff --git a/biome.jsonc b/biome.jsonc index 9d9190960d..0732f9ee0b 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -67,6 +67,7 @@ } }, "suspicious": { + "noConsole": "error", "noUnknownAtRules": { "level": "error", "options": { diff --git a/core/actions/_lib/ActionField.tsx b/core/actions/_lib/ActionField.tsx index 900b7caa27..69a7396a97 100644 --- a/core/actions/_lib/ActionField.tsx +++ b/core/actions/_lib/ActionField.tsx @@ -1,14 +1,22 @@ "use client" +import type React from "react" import type { PropsWithChildren } from "react" -import type { ControllerProps } from "react-hook-form" +import type { + ControllerFieldState, + ControllerProps, + ControllerRenderProps, + FormState, +} from "react-hook-form" import type z from "zod" +import type { Action } from "../types" import type { InputState } from "./ActionFieldJsonataInput" +import type { ActionFormContextContext } from "./ActionForm" -import { useEffect, useId, useState } from "react" +import { memo, useCallback, useEffect, useId, useMemo, useState } from "react" import dynamic from "next/dynamic" import { Braces, TestTube, X } from "lucide-react" -import { Controller, useWatch } from "react-hook-form" +import { Controller } from "react-hook-form" import { Button } from "ui/button" import { ButtonGroup } from "ui/button-group" @@ -49,165 +57,262 @@ type ActionFieldProps = PropsWithChildren<{ /* id for the label */ labelId?: HTMLFormElement["id"] description?: string + // form: UseFormReturn; + schema: z.ZodType + defaultFields: string[] + path?: string + context: ActionFormContextContext + action: Action }> -export function ActionField(props: ActionFieldProps) { - const { form, schema, defaultFields, context, action, path } = useActionForm() - - const fieldName = path ? `${path}.${props.name}` : props.name - - const innerSchema = - "innerType" in schema._def ? schema._def?.innerType : (schema as z.ZodObject) - const schemaShape = innerSchema?.shape ?? {} - const fieldSchema = schemaShape[props.name] as z.ZodType - const required = fieldSchema && !fieldSchema.isOptional() - const isDefaultField = defaultFields.includes(props.name) - const val = useWatch({ control: form.control, name: fieldName }) - const isInitialJsonata = isJsonTemplate(val) - - const [inputState, setInputState] = useState({ - state: isInitialJsonata ? "jsonata" : "normal", - jsonValue: isInitialJsonata ? val : "", - normalValue: isInitialJsonata ? "" : val, - }) - const [isTestOpen, setIsTestOpen] = useState(false) - - const toggleJsonState = () => { - setInputState((prev) => ({ - ...prev, - state: inputState.state === "jsonata" ? "normal" : "jsonata", - })) +export const ActionField = memo( + function ActionField( + props: Omit< + ActionFieldProps, + "form" | "schema" | "defaultFields" | "path" | "context" | "action" + > + ) { + const { action, schema, defaultFields, context, path, form } = useActionForm() + + const fieldName = path ? `${path}.${props.name}` : props.name + return ( + ( + + )} + /> + ) + }, + (prevProps, nextProps) => { + return ( + prevProps.name === nextProps.name && + prevProps.label === nextProps.label && + prevProps.description === nextProps.description && + prevProps.labelId === nextProps.labelId && + prevProps.render === nextProps.render + ) + } +) + +const JSONataToggleButton = memo( + function JSONataToggleButton({ + inputState, + setInputState, + fieldName, + onChange, + }: { + inputState: InputState + setInputState: React.Dispatch> + fieldName: string + onChange: (value: any) => void + }) { + const handleToggle = useCallback(() => { + setInputState((prev) => { + onChange(prev.state === "jsonata" ? prev.normalValue : prev.jsonValue) + return { + ...prev, + state: prev.state === "jsonata" ? "normal" : "jsonata", + } + }) + }, [onChange, setInputState]) + + return ( + + ) + }, + (prevProps, nextProps) => { + return ( + prevProps.inputState.state === nextProps.inputState.state && + prevProps.fieldName === nextProps.fieldName + ) } +) + +const InnerActionField = memo( + function InnerActionField( + props: Omit & { + field: ControllerRenderProps + fieldState: ControllerFieldState + formState: FormState + } + ) { + const innerSchema = + "innerType" in props.schema._def + ? (props.schema._def?.innerType as z.ZodObject) + : (props.schema as z.ZodObject) + const schemaShape = innerSchema?.shape ?? {} + const fieldSchema = schemaShape[props.name] as z.ZodType + const required = fieldSchema && !fieldSchema.isOptional() + const isDefaultField = props.defaultFields.includes(props.name) + const isInitialJsonata = isJsonTemplate(props.field.value) + + const [inputState, setInputState] = useState({ + state: isInitialJsonata ? "jsonata" : "normal", + jsonValue: isInitialJsonata ? props.field.value : "", + normalValue: isInitialJsonata ? "" : props.field.value, + }) + const [isTestOpen, setIsTestOpen] = useState(false) + + useEffect(() => { + setInputState((prev) => ({ + ...prev, + jsonValue: prev.state === "jsonata" ? props.field.value : prev.jsonValue, + normalValue: prev.state === "normal" ? props.field.value : prev.normalValue, + })) + }, [props.field.value]) - useEffect(() => { - setInputState((prev) => ({ - ...prev, - jsonValue: prev.state === "jsonata" ? val : prev.jsonValue, - normalValue: prev.state === "normal" ? val : prev.normalValue, - })) - }, [val]) - - const labelIdMaybe = useId() - const labelId = props.labelId ?? labelIdMaybe - - return ( - { - const showTestButton = - inputState.state === "jsonata" || - (hasTemplateSyntax(p.field.value) && - (context.type === "run" || - context.type === "configure" || - context.type === "automation" || - (context.type === "default" && action.accepts.includes("json")))) - - return ( - -
-
- {props.label && ( - { + if (props.render) { + return props.render({ + field: props.field, + fieldState: props.fieldState, + formState: props.formState, + }) + } + return ( + + ) + }, [ + props.field.value, + props.fieldState.invalid, + props.render, + labelId, + props.field.name, + props.field.onChange, + props.field.onBlur, + props.field.ref, + ]) + + const label = useMemo(() => { + if (!props.label) return null + + return ( + + {props.label} + {required && *} + + ) + }, [props.label, required, labelId]) + + return ( + +
+
+ {label} + + + {props.description ?? fieldSchema.description} + +
+ + {showTestButton && ( + + +
- - {showTestButton && ( - - - - - - {isTestOpen - ? "Close test" - : "Test the result of this field"} - - - )} - - -
- {inputState.state === "jsonata" ? ( - - ) : ( - (props.render?.(p) ?? ( - - )) + {isTestOpen ? : } + + + + {isTestOpen ? "Close test" : "Test the result of this field"} + + )} + + +
+ {inputState.state === "jsonata" ? ( + + ) : ( + rendered + )} - {p.fieldState.invalid && } - {isTestOpen && showTestButton && ( - - )} -
- ) - }} - /> - ) -} + {props.fieldState.invalid && ( + + )} + {isTestOpen && showTestButton && ( + + )} + + ) + }, + (prevProps, nextProps) => { + return ( + prevProps.field.value === nextProps.field.value && + prevProps.fieldState.invalid === nextProps.fieldState.invalid && + prevProps.fieldState.error === nextProps.fieldState.error && + prevProps.fieldState.isTouched === nextProps.fieldState.isTouched && + prevProps.fieldState.isDirty === nextProps.fieldState.isDirty + ) + } +) diff --git a/core/actions/_lib/ActionForm.tsx b/core/actions/_lib/ActionForm.tsx index 11bbe24279..d575e8a9ae 100644 --- a/core/actions/_lib/ActionForm.tsx +++ b/core/actions/_lib/ActionForm.tsx @@ -4,7 +4,7 @@ import type { FieldValues, UseFormReturn } from "react-hook-form" import type { ZodObject, ZodOptional } from "zod" import type { Action } from "../types" -import { createContext, useCallback, useContext, useMemo } from "react" +import { createContext, useCallback, useContext, useMemo, useRef } from "react" import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" @@ -46,20 +46,38 @@ type ActionFormProps = PropsWithChildren<{ context: ActionFormContextContext - onSubmit(values: Record, form: UseFormReturn): Promise - - submitButton: { - text: string - pendingText?: string - successText?: string - errorText?: string - className?: string - } - secondaryButton?: { - text?: string - className?: string - onClick: () => void - } + onSubmit( + values: Record, + form: UseFormReturn, + options?: Record + ): Promise + + submitButton: + | (({ + formState, + submit, + }: { + formState: UseFormReturn["formState"] + submit: (options?: Record) => void + }) => React.ReactNode) + | { + text: string + pendingText?: string + successText?: string + errorText?: string + className?: string + } + secondaryButton?: + | (({ + formState, + }: { + formState: UseFormReturn["formState"] + }) => React.ReactNode) + | { + text?: string + className?: string + onClick: () => void + } }> export const ActionFormContext = createContext(undefined) @@ -90,13 +108,75 @@ export function ActionForm(props: ActionFormProps) { defaultValues, }) + // store options for the current submission + const submitOptionsRef = useRef | undefined>(undefined) + const onSubmit = useCallback( async (data: Record) => { - await props.onSubmit(data, form) + const options = submitOptionsRef.current + submitOptionsRef.current = undefined + await props.onSubmit(data, form, options) }, [props.onSubmit, form] ) + const submitWithOptions = useCallback( + (options?: Record) => { + submitOptionsRef.current = options + form.handleSubmit(onSubmit)() + }, + [form, onSubmit] + ) + + const secondaryButtonElement = useMemo(() => { + if (!props.secondaryButton) { + return null + } + + if (typeof props.secondaryButton === "function") { + return props.secondaryButton({ + formState: form.formState, + }) + } + + return ( + + ) + }, [props.secondaryButton, form.formState]) + + const submitButton = useMemo(() => { + if (!props.submitButton) { + return null + } + + if (typeof props.submitButton === "function") { + return props.submitButton({ + formState: form.formState, + submit: submitWithOptions, + }) + } + + return ( + + ) + }, [props.submitButton, form.formState, submitWithOptions]) + return ( {props.children} - {props.secondaryButton && ( - - )} - - + {secondaryButtonElement} + {submitButton} diff --git a/core/actions/_lib/automations.tsx b/core/actions/_lib/automations.tsx deleted file mode 100644 index 1f5309f589..0000000000 --- a/core/actions/_lib/automations.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import type { AutomationsId } from "db/public" - -import { - ArrowRightFromLine, - ArrowRightToLine, - CalendarClock, - CheckCircle, - Globe, - XCircle, -} from "lucide-react" -import { z } from "zod" - -import { Event } from "db/public" -import { CopyButton } from "ui/copy-button" - -import { defineAutomation } from "~/actions/types" - -export const intervals = ["minute", "hour", "day", "week", "month", "year"] as const -export type Interval = (typeof intervals)[number] - -export const pubInStageForDuration = defineAutomation({ - event: Event.pubInStageForDuration, - additionalConfig: z.object({ - duration: z.number().int().min(1), - interval: z.enum(intervals), - }), - display: { - icon: CalendarClock, - base: "a pub stays in this stage for...", - hydrated: ({ config: { duration, interval } }) => - `a pub stays in this stage for ${duration} ${interval}s`, - }, -}) -export type PubInStageForDuration = typeof pubInStageForDuration - -export const pubLeftStage = defineAutomation({ - event: Event.pubLeftStage, - display: { - icon: ArrowRightFromLine, - base: "a pub leaves this stage", - }, -}) -export type PubLeftStage = typeof pubLeftStage - -export const pubEnteredStage = defineAutomation({ - event: Event.pubEnteredStage, - display: { - icon: ArrowRightToLine, - base: "a pub enters this stage", - }, -}) -export type PubEnteredStage = typeof pubEnteredStage - -export const actionSucceeded = defineAutomation({ - event: Event.actionSucceeded, - display: { - icon: CheckCircle, - base: "a specific action succeeds", - hydrated: ({ config }) => `${config.name} succeeds`, - }, -}) -export type ActionSucceeded = typeof actionSucceeded - -export const actionFailed = defineAutomation({ - event: Event.actionFailed, - display: { - icon: XCircle, - base: "a specific action fails", - hydrated: ({ config }) => `${config.name} fails`, - }, -}) -export type ActionFailed = typeof actionFailed - -export const constructWebhookUrl = (automationId: AutomationsId, communitySlug: string) => - `/api/v0/c/${communitySlug}/site/webhook/${automationId}` - -export const webhook = defineAutomation({ - event: Event.webhook, - display: { - icon: Globe, - base: ({ community }) => ( - - a request is made to{" "} - - {constructWebhookUrl("" as AutomationsId, community.slug)} - - - ), - hydrated: ({ automation, community }) => ( - - a request is made to{" "} - {constructWebhookUrl(automation.id, community.slug)} - - - ), - }, -}) - -export type Automation = - | PubInStageForDuration - | PubLeftStage - | PubEnteredStage - | ActionSucceeded - | ActionFailed - -export type SchedulableEvent = - | Event.pubInStageForDuration - | Event.actionFailed - | Event.actionSucceeded - -export type AutomationForEvent = E extends E - ? Extract - : never - -export type SchedulableAutomation = AutomationForEvent - -export type AutomationConfig = A extends A - ? { - automationConfig: NonNullable["_input"] extends infer RC - ? undefined extends RC - ? null - : RC - : null - actionConfig: Record | null - } - : never - -export type AutomationConfigs = AutomationConfig | undefined diff --git a/core/actions/_lib/evaluateConditions.db.test.ts b/core/actions/_lib/evaluateConditions.db.test.ts new file mode 100644 index 0000000000..e14fb23c29 --- /dev/null +++ b/core/actions/_lib/evaluateConditions.db.test.ts @@ -0,0 +1,369 @@ +import { describe, expect, test } from "vitest" + +import { + Action, + AutomationConditionBlockType, + AutomationEvent, + CoreSchemaType, + MemberRole, +} from "db/public" + +import { mockServerCode } from "~/lib/__tests__/utils" + +const { createForEachMockedTransaction } = await mockServerCode() + +const { getTrx } = createForEachMockedTransaction() + +describe("evaluateConditions", () => { + test("evaluates AND block correctly", async () => { + const _trx = getTrx() + const { seedCommunity } = await import("~/prisma/seed/seedCommunity") + const { stages } = await seedCommunity({ + community: { + name: "test", + slug: "test-and-block", + }, + pubFields: { + Title: { schemaName: CoreSchemaType.String }, + }, + pubTypes: { + "Test Pub": { + Title: { isTitle: true }, + }, + }, + stages: { + "Stage 1": { + automations: { + "Test Automation": { + triggers: [ + { + event: AutomationEvent.pubEnteredStage, + config: {}, + }, + ], + actions: [ + { + action: Action.log, + config: { text: "test" }, + }, + ], + condition: { + type: AutomationConditionBlockType.AND, + items: [ + { + kind: "condition", + type: "jsonata", + expression: '$.pub.title = "test"', + }, + { + kind: "condition", + type: "jsonata", + expression: '$.pub.status = "published"', + }, + ], + }, + }, + }, + }, + }, + users: { + john: { + firstName: "John", + role: MemberRole.admin, + password: "john-password", + email: "john@example.com", + }, + }, + }) + + const { getAutomation } = await import("~/lib/db/queries") + const { evaluateConditions } = await import("./evaluateConditions") + + const automation = await getAutomation(stages["Stage 1"].automations["Test Automation"].id) + if (!automation) { + throw new Error("Automation not found") + } + const condition = automation.condition! + + const resultTrue = await evaluateConditions(condition, { + pub: { title: "test", status: "published" }, + status: "published", + }) + expect(resultTrue.passed).toBe(true) + + const resultFalse = await evaluateConditions(condition, { + pub: { title: "test", status: "draft" }, + status: "draft", + }) + expect(resultFalse.passed).toBe(false) + }) + + test("evaluates OR block correctly", async () => { + const _trx = getTrx() + const { seedCommunity } = await import("~/prisma/seed/seedCommunity") + const { stages } = await seedCommunity({ + community: { + name: "test", + slug: "test-or-block", + }, + pubFields: { + Title: { schemaName: CoreSchemaType.String }, + }, + pubTypes: { + "Test Pub": { + Title: { isTitle: true }, + }, + }, + stages: { + "Stage 1": { + automations: { + "Test Automation": { + triggers: [ + { + event: AutomationEvent.pubEnteredStage, + config: {}, + }, + ], + actions: [ + { + action: Action.log, + config: { text: "test" }, + }, + ], + condition: { + type: AutomationConditionBlockType.OR, + items: [ + { + kind: "condition", + type: "jsonata", + expression: '$.pub.status = "published"', + }, + { + kind: "condition", + type: "jsonata", + expression: '$.pub.status = "draft"', + }, + ], + }, + }, + }, + }, + }, + users: { + john: { + firstName: "John", + role: MemberRole.admin, + password: "john-password", + email: "john@example.com", + }, + }, + }) + + const { getAutomation } = await import("~/lib/db/queries") + const { evaluateConditions } = await import("./evaluateConditions") + + const automation = await getAutomation(stages["Stage 1"].automations["Test Automation"].id) + if (!automation) { + throw new Error("Automation not found") + } + const condition = automation.condition! + + const resultDraft = await evaluateConditions(condition, { pub: { status: "draft" } }) + expect(resultDraft.passed).toBe(true) + + const resultPublished = await evaluateConditions(condition, { + pub: { status: "published" }, + }) + expect(resultPublished.passed).toBe(true) + + const resultArchived = await evaluateConditions(condition, { pub: { status: "archived" } }) + expect(resultArchived.passed).toBe(false) + }) + + test("evaluates NOT block correctly", async () => { + const _trx = getTrx() + const { seedCommunity } = await import("~/prisma/seed/seedCommunity") + const { stages } = await seedCommunity({ + community: { + name: "test", + slug: "test-not-block", + }, + pubFields: { + Title: { schemaName: CoreSchemaType.String }, + }, + pubTypes: { + "Test Pub": { + Title: { isTitle: true }, + }, + }, + stages: { + "Stage 1": { + automations: { + "Test Automation": { + triggers: [ + { + event: AutomationEvent.pubEnteredStage, + config: {}, + }, + ], + actions: [ + { + action: Action.log, + config: { text: "test" }, + }, + ], + condition: { + type: AutomationConditionBlockType.NOT, + items: [ + { + kind: "condition", + type: "jsonata", + expression: '$.pub.status = "archived"', + }, + ], + }, + }, + }, + }, + }, + users: { + john: { + firstName: "John", + role: MemberRole.admin, + password: "john-password", + email: "john@example.com", + }, + }, + }) + + const { getAutomation } = await import("~/lib/db/queries") + const { evaluateConditions } = await import("./evaluateConditions") + + const automation = await getAutomation(stages["Stage 1"].automations["Test Automation"].id) + if (!automation) { + throw new Error("Automation not found") + } + const condition = automation.condition! + + const resultPublished = await evaluateConditions(condition, { + pub: { status: "published" }, + }) + expect(resultPublished.passed).toBe(true) + + const resultArchived = await evaluateConditions(condition, { pub: { status: "archived" } }) + expect(resultArchived.passed).toBe(false) + }) + + test("evaluates nested blocks correctly", async () => { + const _trx = getTrx() + const { seedCommunity } = await import("~/prisma/seed/seedCommunity") + const { stages } = await seedCommunity({ + community: { + name: "test", + slug: "test-nested-blocks", + }, + pubFields: { + Title: { schemaName: CoreSchemaType.String }, + }, + pubTypes: { + "Test Pub": { + Title: { isTitle: true }, + }, + }, + stages: { + "Stage 1": { + automations: { + "Test Automation": { + triggers: [ + { + event: AutomationEvent.pubEnteredStage, + config: {}, + }, + ], + actions: [ + { + action: Action.log, + config: { text: "test" }, + }, + ], + condition: { + type: AutomationConditionBlockType.AND, + items: [ + { + kind: "condition", + type: "jsonata", + expression: '$.pub.title = "test"', + }, + { + kind: "condition", + type: "jsonata", + expression: '$contains($.pub.partial, "de")', + }, + { + kind: "block", + type: AutomationConditionBlockType.OR, + items: [ + { + kind: "condition", + type: "jsonata", + expression: '$.pub.status = "published"', + }, + { + kind: "condition", + type: "jsonata", + expression: '$.pub.status = "draft"', + }, + ], + }, + { + kind: "block", + type: AutomationConditionBlockType.NOT, + items: [ + { + kind: "condition", + type: "jsonata", + expression: '$.pub.status = "archived"', + }, + ], + }, + ], + }, + }, + }, + }, + }, + users: { + john: { + firstName: "John", + role: MemberRole.admin, + password: "john-password", + email: "john@example.com", + }, + }, + }) + + const { getAutomation } = await import("~/lib/db/queries") + const { evaluateConditions } = await import("./evaluateConditions") + + const automation = await getAutomation(stages["Stage 1"].automations["Test Automation"].id) + if (!automation) { + throw new Error("Automation not found") + } + const condition = automation.condition! + + const resultTrue = await evaluateConditions(condition, { + pub: { title: "test", status: "draft", partial: "de" }, + }) + expect(resultTrue.passed).toBe(true) + + const resultFalseTitle = await evaluateConditions(condition, { + pub: { title: "other", status: "draft", partial: "me" }, + }) + expect(resultFalseTitle.passed).toBe(false) + + const resultFalseStatus = await evaluateConditions(condition, { + pub: { title: "test", status: "archived", partial: "de" }, + }) + expect(resultFalseStatus.passed).toBe(false) + }) +}) diff --git a/core/actions/_lib/evaluateConditions.ts b/core/actions/_lib/evaluateConditions.ts new file mode 100644 index 0000000000..863bb0c5ed --- /dev/null +++ b/core/actions/_lib/evaluateConditions.ts @@ -0,0 +1,295 @@ +import type { ConditionBlock } from "db/types" + +import { interpolate } from "@pubpub/json-interpolate" +import { AutomationConditionBlockType } from "db/public" + +type ConditionItem = ConditionBlock["items"][number] +type Condition = Extract + +export type ConditionEvaluation = { + id: string + expression: string + evaluatedTo: unknown + passed: boolean +} + +export type BlockFailureReason = { + blockType: AutomationConditionBlockType + expectedAllTrue?: boolean + expectedAnyTrue?: boolean + expectedFalse?: boolean + failedConditions: ConditionEvaluation[] + failedBlocks: BlockFailureReason[] +} + +export type FailureMessage = { + path: string + message: string + conditions?: ConditionEvaluation[] +} + +export type ConditionEvaluationResult = { + passed: boolean + failureReason?: BlockFailureReason + flatMessages: FailureMessage[] +} + +// collect all conditions from the tree +const collectConditions = (block: ConditionBlock): Condition[] => { + const conditions: Condition[] = [] + + for (const item of block.items) { + if (item.kind === "condition") { + conditions.push(item) + } else { + conditions.push(...collectConditions(item)) + } + } + + return conditions +} + +type EvaluationContext = { + conditionEvaluations: Map + path: string[] +} + +type BlockEvaluationResult = { + passed: boolean + failureReason?: BlockFailureReason +} + +// evaluate a block using pre-computed condition results +const evaluateBlockWithResults = ( + block: ConditionBlock, + context: EvaluationContext +): BlockEvaluationResult => { + const { type, items } = block + + if (items.length === 0) { + return { passed: true } + } + + const itemResults = items.map((item) => { + if (item.kind === "condition") { + const evaluation = context.conditionEvaluations.get(item.id) + return { + passed: evaluation?.passed ?? false, + evaluation, + } + } + return evaluateBlockWithResults(item, context) + }) + + if (type === AutomationConditionBlockType.AND) { + const allPassed = itemResults.every((result) => result.passed) + + if (!allPassed) { + const failedConditions: ConditionEvaluation[] = [] + const failedBlocks: BlockFailureReason[] = [] + + itemResults.forEach((result) => { + if (!result.passed) { + if ("evaluation" in result && result.evaluation) { + failedConditions.push(result.evaluation) + } else if ("failureReason" in result && result.failureReason) { + failedBlocks.push(result.failureReason) + } + } + }) + + return { + passed: false, + failureReason: { + blockType: AutomationConditionBlockType.AND, + expectedAllTrue: true, + failedConditions, + failedBlocks, + }, + } + } + + return { passed: true } + } + + if (type === AutomationConditionBlockType.OR) { + const anyPassed = itemResults.some((result) => result.passed) + + if (!anyPassed) { + const failedConditions: ConditionEvaluation[] = [] + const failedBlocks: BlockFailureReason[] = [] + + itemResults.forEach((result) => { + if ("evaluation" in result && result.evaluation) { + failedConditions.push(result.evaluation) + } else if ("failureReason" in result && result.failureReason) { + failedBlocks.push(result.failureReason) + } + }) + + return { + passed: false, + failureReason: { + blockType: AutomationConditionBlockType.OR, + expectedAnyTrue: true, + failedConditions, + failedBlocks, + }, + } + } + + return { passed: true } + } + + if (type === AutomationConditionBlockType.NOT) { + if (itemResults.length !== 1) { + return { passed: false } + } + + const innerPassed = itemResults[0].passed + + if (innerPassed) { + const failedConditions: ConditionEvaluation[] = [] + const failedBlocks: BlockFailureReason[] = [] + + const result = itemResults[0] + if ("evaluation" in result && result.evaluation) { + failedConditions.push(result.evaluation) + } else if ("failureReason" in result && result.failureReason) { + failedBlocks.push(result.failureReason) + } + + return { + passed: false, + failureReason: { + blockType: AutomationConditionBlockType.NOT, + expectedFalse: true, + failedConditions, + failedBlocks, + }, + } + } + + return { passed: true } + } + + return { passed: false } +} + +// flatten failure reasons into user-friendly messages +const flattenFailureReasons = ( + failureReason: BlockFailureReason, + path: string[] = [] +): FailureMessage[] => { + const messages: FailureMessage[] = [] + const currentPath = [...path, failureReason.blockType] + const pathString = currentPath.join(" > ") + + if (failureReason.expectedAllTrue && failureReason.failedConditions.length > 0) { + const conditionMessages = failureReason.failedConditions + .map((cond) => { + return `expression "${cond.expression}" evaluated to ${JSON.stringify(cond.evaluatedTo)} (expected truthy)` + }) + .join(", ") + + messages.push({ + path: pathString, + message: `AND block requires all conditions to be true, but ${failureReason.failedConditions.length} failed: ${conditionMessages}`, + conditions: failureReason.failedConditions, + }) + } + + if (failureReason.expectedAnyTrue) { + if (failureReason.failedConditions.length > 0) { + const conditionMessages = failureReason.failedConditions + .map((cond) => { + return `"${cond.expression}" = ${JSON.stringify(cond.evaluatedTo)}` + }) + .join(", ") + + messages.push({ + path: pathString, + message: `OR block requires at least one condition to be true, but all ${failureReason.failedConditions.length} failed: ${conditionMessages}`, + conditions: failureReason.failedConditions, + }) + } + } + + if (failureReason.expectedFalse && failureReason.failedConditions.length > 0) { + const conditionMessages = failureReason.failedConditions + .map((cond) => { + return `"${cond.expression}" = ${JSON.stringify(cond.evaluatedTo)}` + }) + .join(", ") + + messages.push({ + path: pathString, + message: `NOT block requires condition to be false, but it was true: ${conditionMessages}`, + conditions: failureReason.failedConditions, + }) + } + + for (const nestedBlock of failureReason.failedBlocks) { + messages.push(...flattenFailureReasons(nestedBlock, currentPath)) + } + + return messages +} + +export const evaluateConditions = async ( + conditions: ConditionBlock, + data: unknown +): Promise => { + // collect all conditions across all nesting levels + const allConditions = collectConditions(conditions) + + // evaluate all jsonata expressions in parallel, keeping raw results + const evaluationPromises = allConditions.map(async (condition) => { + if (condition.type !== "jsonata") { + return { + id: condition.id, + expression: "", + rawResult: null, + booleanResult: false, + } + } + + const rawResult = await interpolate(condition.expression, data) + return { + id: condition.id, + expression: condition.expression, + rawResult, + booleanResult: !!rawResult, + } + }) + + const evaluatedResults = await Promise.all(evaluationPromises) + + // build condition evaluations map + const conditionEvaluations = new Map() + + for (const { id, expression, rawResult, booleanResult } of evaluatedResults) { + conditionEvaluations.set(id, { + id, + expression, + evaluatedTo: rawResult, + passed: booleanResult, + }) + } + + // evaluate the block structure with pre-computed results + const context: EvaluationContext = { + conditionEvaluations, + path: [], + } + + const result = evaluateBlockWithResults(conditions, context) + + const flatMessages = result.failureReason ? flattenFailureReasons(result.failureReason) : [] + + return { + passed: result.passed, + failureReason: result.failureReason, + flatMessages, + } +} diff --git a/core/actions/_lib/runActionInstance.db.test.ts b/core/actions/_lib/runActionInstance.db.test.ts index b3228188be..e42e6b3a94 100644 --- a/core/actions/_lib/runActionInstance.db.test.ts +++ b/core/actions/_lib/runActionInstance.db.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest" -import { Action, ActionRunStatus, CoreSchemaType, Event } from "db/public" +import { Action, ActionRunStatus, AutomationEvent, CoreSchemaType } from "db/public" import { mockServerCode } from "~/lib/__tests__/utils" @@ -33,27 +33,57 @@ const pubTriggerTestSeed = async () => { }, stages: { Submission: { - actions: { + automations: { "1": { - action: Action.log, - config: { - debounce: 1, - }, + triggers: [ + { + event: AutomationEvent.manual, + config: {}, + }, + ], + actions: [ + { + action: Action.log, + config: { + debounce: 1, + }, + }, + ], }, "2": { - action: Action.email, - config: { - recipientEmail: "all@pubpub.org", - body: "Hello", - subject: "Test", - }, + triggers: [ + { + event: AutomationEvent.manual, + config: {}, + }, + ], + actions: [ + { + action: Action.email, + config: { + recipientEmail: "all@pubpub.org", + body: "Hello", + subject: "Test", + }, + }, + ], }, "3": { - action: Action.googleDriveImport, - config: { - folderUrl: "https://drive.google.com/drive/folders/1234567890", - outputField: `${slugName}:title`, - }, + triggers: [ + { + event: AutomationEvent.manual, + config: {}, + }, + ], + actions: [ + { + action: Action.googleDriveImport, + config: { + folderUrl: "https://drive.google.com/drive/folders/1234567890", + outputField: `${slugName}:title`, + }, + }, + ], }, }, }, @@ -72,28 +102,35 @@ const pubTriggerTestSeed = async () => { }) } -describe("runActionInstance", () => { - it("should be able to successfully run the most simple action", async () => { +describe("runAutomation", () => { + it("should be able to successfully run the most simple automation", async () => { const { seedCommunity } = await import("~/prisma/seed/seedCommunity") - const { pubs, actions, community } = await seedCommunity(await pubTriggerTestSeed(), { + const { pubs, stages, community } = await seedCommunity(await pubTriggerTestSeed(), { randomSlug: false, }) - const { runActionInstance } = await import("~/actions/_lib/runActionInstance") + const { runAutomation } = await import("~/actions/_lib/runAutomation") - const logActionInstance = actions.find((a) => a.action === Action.log)! - const result = await runActionInstance({ - actionInstanceId: logActionInstance.id, + const logActionInstance = stages.Submission.automations["1"].actionInstances.find( + (a) => a.action === Action.log + )! + const result = await runAutomation({ pubId: pubs[0].id, - event: Event.pubEnteredStage, + trigger: { + event: AutomationEvent.manual, + config: {}, + }, + manualActionInstancesOverrideArgs: {}, communityId: community.id, stack: [], - actionInstanceArgs: null, + automationId: stages.Submission.automations["1"].id, }) expect(result).toMatchObject({ success: true, - report: "Logged out some data, check your console.", - data: {}, + report: { + report: "Logged out some data, check your console.", + data: {}, + }, }) const actionRuns = await getTrx() @@ -112,72 +149,4 @@ describe("runActionInstance", () => { data: {}, }) }, 10_000) - - it.skip("should properly blame the action run if an action modifies a pub", async () => { - const trx = getTrx() - const { seedCommunity } = await import("~/prisma/seed/seedCommunity") - const { pubs, actions, community, pubFields } = await seedCommunity( - await pubTriggerTestSeed(), - { - randomSlug: false, - } - ) - const { runActionInstance } = await import("~/actions/_lib/runActionInstance") - - const googleDriveImportActionInstance = actions.find( - (a) => a.action === Action.googleDriveImport - )! - - const fakeDocURL = "https://docs.google.com/document/d/1234567890" - const result = await runActionInstance({ - actionInstanceId: googleDriveImportActionInstance.id, - pubId: pubs[0].id, - event: Event.pubEnteredStage, - actionInstanceArgs: { - outputField: `${community.slug}:title`, - docUrl: fakeDocURL, - }, - communityId: community.id, - stack: [], - }) - - expect(result).toEqual({ - success: true, - report: "Successfully imported", - data: {}, - }) - - const actionRun = await trx - .selectFrom("action_runs") - .where("pubId", "=", pubs[0].id) - .where("actionInstanceId", "=", googleDriveImportActionInstance.id) - .selectAll() - .executeTakeFirstOrThrow() - - expect(actionRun?.result).toEqual({ - success: true, - report: "Successfully imported", - data: {}, - }) - - const pubValuesAfterUpdate = await trx - .selectFrom("pub_values") - .where("pubId", "=", pubs[0].id) - .selectAll() - .execute() - - expect(pubValuesAfterUpdate).toHaveLength(2) - const titleValue = pubValuesAfterUpdate.find((v) => v.fieldId === pubFields.Title.id) - - expect(titleValue?.value).toEqual(fakeDocURL) - - const pubValuesHistory = await trx - .selectFrom("pub_values_history") - .where("actionRunId", "=", actionRun.id) - .selectAll() - .execute() - - expect(pubValuesHistory).toHaveLength(1) - expect(pubValuesHistory[0].newRowData?.value).toEqual(fakeDocURL) - }) }) diff --git a/core/actions/_lib/runActionInstance.ts b/core/actions/_lib/runActionInstance.ts deleted file mode 100644 index e8f4f8ae89..0000000000 --- a/core/actions/_lib/runActionInstance.ts +++ /dev/null @@ -1,415 +0,0 @@ -import type { - ActionInstancesId, - ActionRunsId, - CommunitiesId, - PubsId, - StagesId, - UsersId, -} from "db/public" -import type { BaseActionInstanceConfig, Json } from "db/types" -import type { Prettify, XOR } from "utils/types" -import type { ZodError } from "zod" -import type { ClientException, ClientExceptionOptions } from "~/lib/serverActions" -import type { run as logRun } from "../log/run" -import type { ActionSuccess } from "../types" - -import { captureException } from "@sentry/nextjs" -import { sql } from "kysely" - -import { ActionRunStatus, Event } from "db/public" -import { logger } from "logger" - -import { db } from "~/kysely/database" -import { env } from "~/lib/env/env" -import { hydratePubValues } from "~/lib/fields/utils" -import { createLastModifiedBy } from "~/lib/lastModifiedBy" -import { ApiError, getPubsWithRelatedValues } from "~/lib/server" -import { getActionConfigDefaults } from "~/lib/server/actions" -import { MAX_STACK_DEPTH } from "~/lib/server/automations" -import { autoRevalidate } from "~/lib/server/cache/autoRevalidate" -import { getCommunity } from "~/lib/server/community" -import { isClientExceptionOptions } from "~/lib/serverActions" -import { getActionByName } from "../api" -import { ActionConfigBuilder } from "./ActionConfigBuilder" -import { getActionRunByName } from "./getRuns" -import { createPubProxy } from "./pubProxy" -import { scheduleActionInstances } from "./scheduleActionInstance" - -export type ActionInstanceRunResult = (ClientException | ClientExceptionOptions | ActionSuccess) & { - stack: ActionRunsId[] -} - -export type RunActionInstanceArgs = Prettify< - { - communityId: CommunitiesId - actionInstanceId: ActionInstancesId - /** - * extra params passed to the action instance - * for now these are the manual arguments when running the action - * or the config for an automation - */ - actionInstanceArgs: Record | null - stack: ActionRunsId[] - scheduledActionRunId?: ActionRunsId - } & XOR<{ event: Event }, { userId: UsersId }> & - XOR<{ pubId: PubsId }, { json: Json }> -> - -const getActionInstance = (actionInstanceId: ActionInstancesId, trx = db) => - trx - .selectFrom("action_instances") - .where("action_instances.id", "=", actionInstanceId) - .select((eb) => [ - "id", - eb.fn.coalesce("config", sql`'{}'`).as("config"), - "createdAt", - "updatedAt", - "stageId", - "action", - "name", - ]) - .executeTakeFirst() - -const _runActionInstance = async ( - args: RunActionInstanceArgs & { - actionInstance: Exclude>, undefined> - actionRunId: ActionRunsId - } -): Promise => { - const isActionUserInitiated = "userId" in args - - const stack = [...args.stack, args.actionRunId] - - const action = getActionByName(args.actionInstance.action) - const [actionRun, actionDefaults, pub, community] = await Promise.all([ - getActionRunByName(args.actionInstance.action), - getActionConfigDefaults(args.communityId, args.actionInstance.action).executeTakeFirst(), - args.json - ? null - : getPubsWithRelatedValues( - { - pubId: args.pubId!, - communityId: args.communityId, - userId: isActionUserInitiated ? args.userId : undefined, - }, - { - depth: 3, - withPubType: true, - withStage: true, - } - ), - getCommunity(args.communityId), - ]) - - if (!community) { - return { - error: "Community not found", - stack, - } - } - - if (!args.json && !pub) { - return { - error: "No input found", - stack, - } - } - - if (!actionRun || !action) { - return { - error: "Action not found", - stack, - } - } - - const actionConfigBuilder = new ActionConfigBuilder(args.actionInstance.action) - .withConfig(args.actionInstance.config as Record) - .withOverrides(args.actionInstanceArgs as Record) - .withDefaults(actionDefaults?.config as Record) - .validate() - - const inputPubInput = pub - ? { - ...pub, - values: hydratePubValues(pub.values), - } - : null - - let config = null - const mergedConfig = actionConfigBuilder.getMergedConfig() - const actionForInterpolation = { - ...args.actionInstance, - config: mergedConfig, - } - if (inputPubInput) { - const thing = { - pub: createPubProxy(inputPubInput, community?.slug), - action: actionForInterpolation, - } - - const interpolated = await actionConfigBuilder.interpolate(thing) - - const result = interpolated.validate().getResult() - - if (!result.success) { - logger.error("Invalid action configuration", { - // config: result.config, - error: result.error.message, - code: result.error.code, - cause: result.error.zodError, - }) - - return { - title: "Invalid action configuration", - error: result.error.message, - cause: result.error.zodError as ZodError, - issues: result.error.zodError?.issues, - stack, - } - } - - config = result.config - } else { - const thing = { json: args.json, action: actionForInterpolation } - - const result = (await actionConfigBuilder.interpolate(thing)).getResult() - if (!result.success) { - return { - title: "Invalid action configuration", - error: result.error.message, - cause: result.error.zodError ?? result.error.cause, - issues: result.error.zodError?.issues, - stack, - } - } - config = result.config - } - - const lastModifiedBy = createLastModifiedBy({ - actionRunId: args.actionRunId, - }) - - const jsonOrPubId = args.pubId ? { pubId: args.pubId } : { json: args.json! } - try { - // just hard cast it to one option so we at least have some typesafety - const result = await (actionRun as typeof logRun)({ - // FIXME: get rid of any - config: config as any, - ...(inputPubInput - ? { pub: inputPubInput } - : { json: args.json ?? ({} as Record) }), - stageId: args.actionInstance.stageId, - communityId: args.communityId, - lastModifiedBy, - actionRunId: args.actionRunId, - userId: isActionUserInitiated ? args.userId : undefined, - actionInstance: args.actionInstance, - }) - - if (isClientExceptionOptions(result)) { - await scheduleActionInstances({ - stageId: args.actionInstance.stageId, - event: Event.actionFailed, - stack, - sourceActionInstanceId: args.actionInstance.id, - ...jsonOrPubId, - }) - return { ...result, stack } - } - - await scheduleActionInstances({ - stageId: args.actionInstance.stageId, - event: Event.actionSucceeded, - stack, - sourceActionInstanceId: args.actionInstance.id, - ...jsonOrPubId, - }) - - return { ...result, stack } - } catch (error) { - captureException(error) - logger.error(error) - - await scheduleActionInstances({ - stageId: args.actionInstance.stageId, - event: Event.actionFailed, - stack, - sourceActionInstanceId: args.actionInstance.id, - ...jsonOrPubId, - }) - - return { - title: "Failed to run action", - error: error.message, - stack, - } - } -} - -export async function runActionInstance(args: RunActionInstanceArgs, trx = db) { - if (args.stack.length > MAX_STACK_DEPTH) { - throw new Error( - `Action instance stack depth of ${args.stack.length} exceeds the maximum allowed depth of ${MAX_STACK_DEPTH}` - ) - } - const actionInstance = await getActionInstance(args.actionInstanceId) - - if (actionInstance === undefined) { - return { - error: "Action instance not found", - stack: args.stack, - } - } - - if (env.FLAGS?.get("disabled-actions").includes(actionInstance.action)) { - return { ...ApiError.FEATURE_DISABLED, stack: args.stack } - } - - if (!actionInstance.action) { - return { - error: "Action not found", - stack: args.stack, - } - } - - const isActionUserInitiated = "userId" in args - - // we need to first create the action run, - // in case the action modifies the pub and needs to pass the lastModifiedBy field - // which in this case would be `action-run:` - - const actionRuns = await autoRevalidate( - trx - .insertInto("action_runs") - .values((eb) => ({ - id: args.scheduledActionRunId, - actionInstanceId: args.actionInstanceId, - pubId: args.pubId, - json: args.json, - userId: isActionUserInitiated ? args.userId : null, - result: { scheduled: `Action to be run immediately` }, - // we are setting it to `scheduled` very briefly - status: ActionRunStatus.scheduled, - // this is a bit hacky, would be better to pass this around methinks - config: - args.actionInstanceArgs ?? - eb - .selectFrom("action_instances") - .select("config") - .where("action_instances.id", "=", args.actionInstanceId), - params: args, - event: isActionUserInitiated ? undefined : args.event, - sourceActionRunId: args.stack.at(-1), - })) - .returningAll() - // conflict should only happen if a scheduled action is excecuted - // not on user initiated actions or on other events - .onConflict((oc) => - oc.column("id").doUpdateSet({ - params: args, - event: "userId" in args ? undefined : args.event, - }) - ) - ).execute() - - if (actionRuns.length > 1) { - const errorMessage: ActionInstanceRunResult = { - title: "Action run failed", - error: `Multiple scheduled action runs found for pub ${args.pubId} and action instance ${args.actionInstanceId}. This should never happen.`, - cause: `Multiple scheduled action runs found for pub ${args.pubId} and action instance ${args.actionInstanceId}. This should never happen.`, - stack: args.stack, - } - - await autoRevalidate( - trx - .updateTable("action_runs") - .set({ - status: ActionRunStatus.failure, - result: errorMessage, - }) - .where( - "id", - "in", - actionRuns.map((ar) => ar.id) - ) - ).execute() - - throw new Error( - `Multiple scheduled action runs found for pub ${args.pubId} and action instance ${args.actionInstanceId}. This should never happen.` - ) - } - - const actionRun = actionRuns[0] - - const result = await _runActionInstance({ ...args, actionInstance, actionRunId: actionRun.id }) - - const status = isClientExceptionOptions(result) - ? ActionRunStatus.failure - : ActionRunStatus.success - - logger[status === ActionRunStatus.failure ? "error" : "info"]({ - msg: "Action run finished", - pubId: args.pubId, - actionInstanceId: args.actionInstanceId, - status, - result, - }) - - // update the action run with the result - await autoRevalidate( - trx - .updateTable("action_runs") - .set({ status, result }) - .where("id", "=", args.scheduledActionRunId ?? actionRun.id) - ).executeTakeFirstOrThrow( - () => - new Error( - `Failed to update action run ${actionRun.id} for pub ${args.pubId} and action instance ${args.actionInstanceId}` - ) - ) - - return result -} - -export const runInstancesForEvent = async ( - pubId: PubsId, - stageId: StagesId, - event: Event, - communityId: CommunitiesId, - stack: ActionRunsId[], - trx = db -) => { - const instances = await trx - .selectFrom("action_instances") - .innerJoin("automations", "automations.actionInstanceId", "action_instances.id") - .select([ - "action_instances.id as actionInstanceId", - "automations.config as automationConfig", - "action_instances.name as actionInstanceName", - ]) - .where("automations.event", "=", event) - .where("action_instances.stageId", "=", stageId) - .execute() - - const results = await Promise.all( - instances.map(async (instance) => { - return { - actionInstanceId: instance.actionInstanceId, - actionInstanceName: instance.actionInstanceName, - result: await runActionInstance( - { - pubId, - communityId, - actionInstanceId: instance.actionInstanceId, - event, - actionInstanceArgs: instance.automationConfig ?? null, - stack, - }, - trx - ), - } - }) - ) - - return results -} diff --git a/core/actions/_lib/runAutomation.ts b/core/actions/_lib/runAutomation.ts new file mode 100644 index 0000000000..393ee22a19 --- /dev/null +++ b/core/actions/_lib/runAutomation.ts @@ -0,0 +1,592 @@ +import type { ProcessedPub } from "contracts" +import type { Database } from "db/Database" +import type { + ActionInstancesId, + ActionRunsId, + AutomationEvent, + AutomationRunsId, + AutomationsId, + Communities, + CommunitiesId, + PubsId, + StagesId, + UsersId, +} from "db/public" +import type { BaseActionInstanceConfig, FullAutomation, Json } from "db/types" +import type { Kysely } from "kysely" +import type { ZodError } from "zod" +import type { run as logRun } from "../log/run" +import type { ActionSuccess } from "../types" + +import { captureException } from "@sentry/nextjs" +import { jsonArrayFrom } from "kysely/helpers/postgres" + +import { ActionRunStatus } from "db/public" +import { logger } from "logger" +import { expect } from "utils" +import { tryCatch } from "utils/try-catch" + +import { + type ActionRunResult, + type AutomationRunResult, + getActionRunStatusFromResult, + isActionFailure, + isActionSuccess, +} from "~/actions/results" +import { db } from "~/kysely/database" +import { getAutomation } from "~/lib/db/queries" +import { env } from "~/lib/env/env" +import { createLastModifiedBy } from "~/lib/lastModifiedBy" +import { ApiError, getPubsWithRelatedValues } from "~/lib/server" +import { getActionConfigDefaults, getAutomationRunById } from "~/lib/server/actions" +import { MAX_STACK_DEPTH } from "~/lib/server/automations" +import { autoRevalidate } from "~/lib/server/cache/autoRevalidate" +import { getCommunity } from "~/lib/server/community" +import { type CommunityStage, getStages } from "~/lib/server/stages" +import { isClientExceptionOptions } from "~/lib/serverActions" +import { getActionByName } from "../api" +import { ActionConfigBuilder } from "./ActionConfigBuilder" +import { evaluateConditions } from "./evaluateConditions" +import { getActionRunByName } from "./getRuns" +import { createPubProxy } from "./pubProxy" + +export type ActionInstanceRunResult = ActionRunResult + +export type RunAutomationArgs = { + automationId: AutomationsId + scheduledAutomationRunId?: AutomationRunsId + trigger: { + event: AutomationEvent + config: Record | null + } + // overrides when running manually + manualActionInstancesOverrideArgs: { + [actionInstanceId: ActionInstancesId]: Record | null + } | null + // when true, skip condition evaluation (requires overrideAutomationConditions capability) + skipConditionCheck?: boolean + userId?: UsersId + stack: AutomationRunsId[] + communityId: CommunitiesId + pubId?: PubsId + json?: Json +} + +export type RunActionInstanceArgs = { + automation: FullAutomation + community: Communities + stage: CommunityStage + actionInstance: FullAutomation["actionInstances"][number] + /** + * extra params passed to the action instance + * these are provided when running the action manually + */ + manualActionInstanceOverrideArgs: Record | null + // stack: ActionRunsId[]; + actionRunId: ActionRunsId + pub: + | ProcessedPub<{ + withPubType: true + withRelatedPubs: true + withStage: false + withValues: true + }> + | undefined + json: Json | undefined + stageId: StagesId + userId?: UsersId +} + +/** + * run a singular action instance on an automation + */ +const runActionInstance = async (args: RunActionInstanceArgs): Promise => { + const action = getActionByName(args.actionInstance.action) + const pub = args.pub + + const [actionRun, actionDefaults] = await Promise.all([ + getActionRunByName(args.actionInstance.action), + getActionConfigDefaults(args.community.id, args.actionInstance.action).executeTakeFirst(), + ]) + + if (!args.json && !pub) { + logger.error("No input found", { + actionInstance: args.actionInstance, + pub, + json: args.json, + }) + return { + success: false, + error: "No input found", + config: args.actionInstance.config as BaseActionInstanceConfig, + } + } + + if (!actionRun || !action) { + logger.error("Action not found", { + actionInstance: args.actionInstance, + actionRun, + action, + }) + return { + success: false, + error: "Action not found", + config: args.actionInstance.config as BaseActionInstanceConfig, + } + } + + const actionConfigBuilder = new ActionConfigBuilder(args.actionInstance.action) + .withConfig(args.actionInstance.config as Record) + .withOverrides(args.manualActionInstanceOverrideArgs ?? {}) + .withDefaults(actionDefaults?.config as Record) + .validate() + const mergedConfig = actionConfigBuilder.getMergedConfig() + const actionForInterpolation = { + ...args.actionInstance, + config: mergedConfig, + } + + const interpolationData = pub + ? { + pub: createPubProxy(pub, args.community.slug), + stage: args.stage, + action: actionForInterpolation, + } + : { json: args.json, action: actionForInterpolation, stage: args.stage } + + const interpolated = await actionConfigBuilder.interpolate(interpolationData) + + const result = interpolated.validate().getResult() + + if (!result.success) { + logger.error("Invalid action configuration", { + // config: result.config, + error: result.error.message, + code: result.error.code, + cause: result.error.zodError, + }) + + const failConfig = interpolated.getResult() + const resultConfig = failConfig.success ? failConfig.config : args.actionInstance.config + + return { + success: false, + title: "Invalid action configuration", + error: result.error.message, + cause: result.error.zodError as ZodError, + config: resultConfig, + } + } + + const config = result.config + + const lastModifiedBy = createLastModifiedBy({ + actionRunId: args.actionRunId, + }) + + try { + // just hard cast it to one option so we at least have some typesafety + const result = await (actionRun as typeof logRun)({ + // FIXME: get rid of any + config: config as any, + ...(pub ? { pub } : { json: args.json ?? ({} as Record) }), + stageId: args.stageId, + communityId: args.community.id, + lastModifiedBy, + actionRunId: args.actionRunId, + userId: args.userId, + automation: args.automation, + }) + + if (isActionSuccess(result)) { + return { ...result, config } + } + + if (isActionFailure(result)) { + return { ...result, config } + } + + // handle legacy ClientExceptionOptions format + if (isClientExceptionOptions(result)) { + return { + success: false, + title: result.title, + error: result.error, + cause: result.cause, + config, + } + } + + // handle legacy ActionSuccess format + return { + success: true, + report: (result as ActionSuccess).report, + data: (result as ActionSuccess).data, + config, + } + } catch (error) { + captureException(error) + logger.error(error) + + return { + success: false, + title: "Failed to run action", + error: error.message, + cause: error, + config: config, + } + } +} + +export async function runAutomation( + args: RunAutomationArgs, + trx = db +): Promise { + if (args.stack.length > MAX_STACK_DEPTH) { + throw new Error( + `Action instance stack depth of ${args.stack.length} exceeds the maximum allowed depth of ${MAX_STACK_DEPTH}` + ) + } + + const [pub, automation, community] = await Promise.all([ + args.pubId + ? await getPubsWithRelatedValues( + { pubId: args.pubId, communityId: args.communityId }, + { + withPubType: true, + withRelatedPubs: true, + withStage: false, + withValues: true, + depth: 3, + } + ) + : null, + getAutomation(args.automationId), + getCommunity(args.communityId), + ]) + + if (!automation) { + throw new Error(`Automation ${args.automationId} not found`) + } + // annoying that this requires an extra await + + const stage = await getStages({ + communityId: args.communityId, + stageId: expect(automation.stageId, "Can't run automation without a stage"), + userId: null, + }).executeTakeFirstOrThrow(() => new Error(`Stage ${automation.stageId} not found`)) + + if ( + automation.actionInstances.some((ai) => + env.FLAGS?.get("disabled-actions").includes(ai.action) + ) + ) { + return { ...ApiError.FEATURE_DISABLED, stack: args.stack, actionRuns: [], success: false } + } + + if (!community) { + throw new Error(`Community ${args.communityId} not found`) + } + + // check if we need to evaluate conditions at execution time + if (automation?.condition && !args.skipConditionCheck) { + const automationTiming = automation.conditionEvaluationTiming as string | null | undefined + const shouldEvaluateNow = + automationTiming === "onExecution" || + automationTiming === "both" || + // if no timing is set, default to evaluating at execution time for backwards compatibility + !automationTiming + + if (shouldEvaluateNow) { + const input = pub + ? { pub: createPubProxy(pub, community?.slug) } + : { json: args.json ?? ({} as Json) } + const [error, evaluationResult] = await tryCatch( + evaluateConditions(automation.condition, input) + ) + + if (error) { + if (args.scheduledAutomationRunId) { + const existingAutomationRun = await getAutomationRunById( + args.communityId, + args.scheduledAutomationRunId + ).executeTakeFirstOrThrow( + () => new Error(`Automation run ${args.scheduledAutomationRunId} not found`) + ) + + await insertAutomationRun(trx, { + automationId: automation.id, + actionRuns: existingAutomationRun.actionRuns.map((ar) => ({ + config: ar.config as BaseActionInstanceConfig, + result: { error: error.message }, + status: ActionRunStatus.failure, + actionInstanceId: expect( + ar.actionInstanceId, + `Action instance id is required for action run ${ar.id} when creating automation run` + ), + id: ar.id, + })), + pubId: pub?.id, + json: args.json, + communityId: args.communityId, + stack: args.stack, + scheduledAutomationRunId: args.scheduledAutomationRunId, + trigger: args.trigger, + userId: "userId" in args ? args.userId : undefined, + }) + } + + return { + success: false, + title: "Failed to evaluate conditions", + error: error.message, + stack: args.stack, + actionRuns: [], + } + } + + if (!evaluationResult.passed) { + logger.debug("Automation condition not met at execution time", { + automationId: automation.id, + conditionEvaluationTiming: automationTiming, + condition: automation.condition, + failureReason: evaluationResult.failureReason, + failureMessages: evaluationResult.flatMessages, + }) + return { + success: false, + actionRuns: [], + title: "Automation condition not met", + error: evaluationResult.flatMessages.map((m) => m.message).join(", "), + stack: args.stack, + } + } + + logger.debug("Automation condition met at execution time", { + automationId: automation.id, + conditionEvaluationTiming: automationTiming, + }) + } + } + + const isActionUserInitiated = "userId" in args + + const existingAutomationRun = args.scheduledAutomationRunId + ? await getAutomationRunById( + args.communityId, + args.scheduledAutomationRunId + ).executeTakeFirstOrThrow( + () => new Error(`Automation run ${args.scheduledAutomationRunId} not found`) + ) + : null + // we need to first create the action run, + // in case the action modifies the pub and needs to pass the lastModifiedBy field + // which in this case would be `action-run:` + const automationRun = await insertAutomationRun(trx, { + automationId: args.automationId, + actionRuns: automation.actionInstances.map((ai) => ({ + id: existingAutomationRun?.actionRuns.find((ar) => ar.actionInstanceId === ai.id)?.id, + actionInstanceId: ai.id, + config: ai.config, + result: { scheduled: `Action to be run immediately` }, + status: ActionRunStatus.scheduled, + })), + pubId: pub?.id, + json: args.json as Json, + communityId: args.communityId, + stack: args.stack, + scheduledAutomationRunId: args.scheduledAutomationRunId, + trigger: args.trigger, + userId: isActionUserInitiated ? args.userId : undefined, + }) + + const results: (ActionRunResult & { + actionInstance: FullAutomation["actionInstances"][number] + actionRunId: ActionRunsId + })[] = await Promise.all( + automation.actionInstances.map(async (ai) => { + const correcspondingActionRun = automationRun.actionRuns.find( + (ar) => ar.actionInstanceId === ai.id + ) + if (!correcspondingActionRun) { + throw new Error(`Action run not found for action instance ${ai.id}`) + } + const result = await runActionInstance({ + ...args, + actionInstance: ai, + actionRunId: correcspondingActionRun.id, + stageId: expect(automation.stageId), + community, + manualActionInstanceOverrideArgs: + args.manualActionInstancesOverrideArgs?.[ai.id] ?? null, + json: args.json, + stage, + pub: pub ?? undefined, + automation, + }) + + logger[result.success === false ? "error" : "info"]({ + msg: "Automation run finished", + pubId: args.pubId, + actionInstanceId: ai.id, + status: result.success ? ActionRunStatus.success : ActionRunStatus.failure, + result, + }) + + if (result.success === false) { + return { + success: false, + title: "Failed to run action", + error: result.error, + cause: result.cause, + config: result.config, + actionInstance: ai, + actionRunId: correcspondingActionRun.id, + } + } + + return { + ...result, + actionInstance: ai, + actionRunId: correcspondingActionRun.id, + } + }) + ) + + const finalAutomationRun = await insertAutomationRun(trx, { + automationId: args.automationId, + actionRuns: results.map(({ actionRunId, actionInstance, ...result }) => ({ + id: actionRunId, + actionInstanceId: actionInstance.id, + config: result.config, + status: getActionRunStatusFromResult(result), + result: result, + })), + pubId: args.pubId, + json: args.json, + trigger: args.trigger, + communityId: args.communityId, + stack: args.stack, + scheduledAutomationRunId: automationRun.id, + userId: isActionUserInitiated ? args.userId : undefined, + }) + + const success = results.every((r) => r.success === true) + + if (!success) { + return { + success: false, + title: "Automation run failed", + error: `${results + .filter((r) => r.success === false) + .map((r) => r.error) + .join(", ")}`, + stack: [...args.stack, finalAutomationRun.id], + actionRuns: results, + } + } + + return { + success: true, + title: "Automation run finished", + stack: [...args.stack, finalAutomationRun.id], + actionRuns: results, + data: results.map((r) => r.data).reduce((acc, curr) => ({ ...acc, ...curr }), {}), + } +} + +export async function insertAutomationRun( + trx: Kysely, + args: { + automationId: AutomationsId + actionRuns: { + id?: ActionRunsId + actionInstanceId: ActionInstancesId + config: BaseActionInstanceConfig + result: Record + status: ActionRunStatus + }[] + pubId?: PubsId + json?: Json + trigger: { + event: AutomationEvent + config: Record | null + } + communityId: CommunitiesId + stack: AutomationRunsId[] + scheduledAutomationRunId?: AutomationRunsId + userId?: UsersId + } +) { + console.log("args.actionRuns _________________") + console.log(args.actionRuns) + const automatonRun = await autoRevalidate( + trx + .with("automationRun", (trx) => + trx + .insertInto("automation_runs") + .values({ + id: args.scheduledAutomationRunId, + automationId: args.automationId, + sourceUserId: args.userId, + inputJson: args.json, + inputPubId: args.pubId, + triggerConfig: args.trigger.config, + triggerEvent: args.trigger.event, + sourceAutomationRunId: args.stack.at(-1), + }) + .returningAll() + // conflict should only happen if a scheduled action is excecuted + // not on user initiated actions or on other events + .onConflict((oc) => + oc.column("id").doUpdateSet({ + triggerConfig: args.trigger.config, + triggerEvent: args.trigger.event, + }) + ) + ) + .with("actionRuns", (trx) => + trx + .insertInto("action_runs") + .values((eb) => + args.actionRuns.map((ai) => ({ + id: ai.id, + automationRunId: eb + .selectFrom("automationRun") + .select("automationRun.id") + .where("automationRun.automationId", "=", args.automationId) + .limit(1), + actionInstanceId: ai.actionInstanceId, + pubId: args.pubId, + json: args.json, + userId: args.userId, + config: ai.config, + event: args.trigger.event, + status: ai.status, + result: ai.result, + })) + ) + .returningAll() + // update status and result, and config if it had changed in the meantime + .onConflict((oc) => + oc.column("id").doUpdateSet((eb) => ({ + status: eb.ref("excluded.status"), + result: eb.ref("excluded.result"), + config: eb.ref("excluded.config"), + })) + ) + ) + .selectFrom("automationRun") + .selectAll("automationRun") + .select((eb) => + jsonArrayFrom( + eb + .selectFrom("actionRuns") + .selectAll("actionRuns") + .whereRef("actionRuns.automationRunId", "=", "automationRun.id") + ).as("actionRuns") + ) + ).executeTakeFirstOrThrow() + + return automatonRun +} diff --git a/core/actions/_lib/scheduleActionInstance.ts b/core/actions/_lib/scheduleActionInstance.ts deleted file mode 100644 index 70a94393f6..0000000000 --- a/core/actions/_lib/scheduleActionInstance.ts +++ /dev/null @@ -1,172 +0,0 @@ -import type { Json } from "contracts" -import type { ActionInstancesId, ActionRunsId, PubsId, StagesId } from "db/public" -import type { GetEventAutomationOptions } from "~/lib/db/queries" -import type { SchedulableAutomation } from "./automations" - -import { ActionRunStatus, Event } from "db/public" -import { logger } from "logger" - -import { db } from "~/kysely/database" -import { addDuration } from "~/lib/dates" -import { getStageAutomations } from "~/lib/db/queries" -import { autoRevalidate } from "~/lib/server/cache/autoRevalidate" -import { getCommunitySlug } from "~/lib/server/cache/getCommunitySlug" -import { getJobsClient, getScheduledActionJobKey } from "~/lib/server/jobs" - -type Shared = { - stageId: StagesId - stack: ActionRunsId[] - /* Config for the action instance */ - config?: Record | null -} & GetEventAutomationOptions - -type ScheduleActionInstanceForPubOptions = Shared & { - pubId: PubsId - json?: never -} - -type ScheduleActionInstanceGenericOptions = Shared & { - pubId?: never - json: Json -} - -type ScheduleActionInstanceOptions = - | ScheduleActionInstanceForPubOptions - | ScheduleActionInstanceGenericOptions - -export const scheduleActionInstances = async (options: ScheduleActionInstanceOptions) => { - if (!options.stageId) { - throw new Error("StageId is required") - } - - if (!options.pubId && !options.json) { - throw new Error("PubId or body is required") - } - - const [automations, jobsClient] = await Promise.all([ - getStageAutomations(options.stageId, options).execute(), - getJobsClient(), - ]) - - if (!automations.length) { - logger.debug({ - msg: `No action instances found for stage ${options.stageId}. Most likely this is because a Pub is moved into a stage without action instances.`, - pubId: options.pubId, - stageId: options.stageId, - automations, - }) - return - } - - const validAutomations = automations - .filter( - (automation): automation is typeof automation & SchedulableAutomation => - automation.event === Event.actionFailed || - automation.event === Event.actionSucceeded || - automation.event === Event.webhook || - (automation.event === Event.pubInStageForDuration && - Boolean( - typeof automation.config === "object" && - automation.config && - "duration" in automation.config && - automation.config.duration && - "interval" in automation.config && - automation.config.interval - )) - ) - .map((automation) => ({ - ...automation, - duration: automation.config?.automationConfig?.duration || 0, - interval: automation.config?.automationConfig?.interval || "minute", - })) - - const results = await Promise.all( - validAutomations.flatMap(async (automation) => { - const runAt = addDuration({ - duration: automation.duration, - interval: automation.interval, - }).toISOString() - - const scheduledActionRun = await autoRevalidate( - db - .insertInto("action_runs") - .values({ - actionInstanceId: automation.actionInstance.id, - pubId: options.pubId, - json: options.json, - status: ActionRunStatus.scheduled, - config: options.config ?? automation.actionInstance.config, - result: { scheduled: `Action scheduled for ${runAt}` }, - event: automation.event, - sourceActionRunId: options.stack.at(-1), - }) - .returning("id") - ).executeTakeFirstOrThrow() - - const job = await jobsClient.scheduleAction({ - actionInstanceId: automation.actionInstance.id, - duration: automation.duration, - interval: automation.interval, - stageId: options.stageId, - community: { - slug: await getCommunitySlug(), - }, - stack: options.stack, - scheduledActionRunId: scheduledActionRun.id, - event: automation.event, - ...(options.pubId ? { pubId: options.pubId } : { json: options.json! }), - config: options.config ?? automation.actionInstance.config ?? null, - }) - - return { - result: job, - actionInstanceId: automation.actionInstance.id, - actionInstanceName: automation.actionInstance.name, - runAt, - } - }) - ) - - return results -} - -// FIXME: this should be updated to allow unscheduling jobs which aren't pub based -export const unscheduleAction = async ({ - actionInstanceId, - stageId, - pubId, - event, -}: { - actionInstanceId: ActionInstancesId - stageId: StagesId - pubId: PubsId - event: Omit -}) => { - const jobKey = getScheduledActionJobKey({ - stageId, - actionInstanceId, - pubId, - event: event as Event, - }) - try { - const jobsClient = await getJobsClient() - await jobsClient.unscheduleJob(jobKey) - - // TODO: this should probably be set to "canceled" instead of deleting the run - await autoRevalidate( - db - .deleteFrom("action_runs") - .where("actionInstanceId", "=", actionInstanceId) - .where("pubId", "=", pubId) - .where("action_runs.status", "=", ActionRunStatus.scheduled) - ).execute() - - logger.debug({ msg: "Unscheduled action", actionInstanceId, stageId, pubId }) - } catch (error) { - logger.error(error) - return { - error: "Failed to unschedule action", - cause: error, - } - } -} diff --git a/core/actions/_lib/scheduleAutomations.ts b/core/actions/_lib/scheduleAutomations.ts new file mode 100644 index 0000000000..ec01faf89f --- /dev/null +++ b/core/actions/_lib/scheduleAutomations.ts @@ -0,0 +1,219 @@ +import type { AutomationRunsId, AutomationsId, CommunitiesId, PubsId, StagesId } from "db/public" +import type { BaseActionInstanceConfig } from "db/types" + +import { ActionRunStatus, AutomationEvent, ConditionEvaluationTiming } from "db/public" +import { logger } from "logger" +import { expect } from "utils" + +import { db } from "~/kysely/database" +import { addDuration } from "~/lib/dates" +import { getAutomation } from "~/lib/db/queries" +import { getAutomationRunById } from "~/lib/server/actions" +import { findCommunityBySlug } from "~/lib/server/community" +import { getJobsClient, getScheduledAutomationJobKey } from "~/lib/server/jobs" +import { getPubsWithRelatedValues } from "~/lib/server/pub" +import { evaluateConditions } from "./evaluateConditions" +import { createPubProxy } from "./pubProxy" +import { insertAutomationRun } from "./runAutomation" + +export const scheduleDelayedAutomation = async ({ + automationId, + pubId, + stack, +}: { + automationId: AutomationsId + pubId: PubsId + stack: AutomationRunsId[] +}): Promise<{ + automationId: string + runAt: string +}> => { + const community = await findCommunityBySlug() + if (!community) { + throw new Error("Community not found") + } + + const automation = await getAutomation(automationId) + if (!automation) { + throw new Error(`Automation ${automationId} not found`) + } + + const trigger = automation.triggers.find( + (t) => t.event === AutomationEvent.pubInStageForDuration + ) + + // validate this is a pubInStageForDuration automation with proper config + if (!trigger) { + throw new Error(`Automation ${automationId} is not a pubInStageForDuration automation`) + } + + const config = trigger.config as Record | null + if (!config || typeof config !== "object" || !config.duration || !config.interval) { + throw new Error( + `Automation ${automation.name} (${automationId}) in Community ${community.slug} missing duration/interval configuration. Is ${JSON.stringify(config)}` + ) + } + + const duration = config.duration as number + const interval = config.interval as "minute" | "hour" | "day" | "week" | "month" | "year" + + // check if we need to evaluate conditions before scheduling + const automationTiming = automation.conditionEvaluationTiming as string | null | undefined + const shouldEvaluateNow = + automationTiming === ConditionEvaluationTiming.onTrigger || + automationTiming === ConditionEvaluationTiming.both + + const condition = automation.condition + + if (shouldEvaluateNow && condition) { + const pub = await getPubsWithRelatedValues( + { pubId, communityId: community.id }, + { + withPubType: true, + withRelatedPubs: true, + withStage: true, + withValues: true, + depth: 3, + } + ) + + if (!pub) { + throw new Error(`Pub ${pubId} not found`) + } + + const input = { pub: createPubProxy(pub, community.slug) } + const evaluationResult = await evaluateConditions(condition, input) + + if (!evaluationResult.passed) { + logger.info({ + msg: "Skipping automation scheduling - conditions not met at trigger time", + automationId, + conditionEvaluationTiming: automationTiming, + failureReason: evaluationResult.failureReason, + failureMessages: evaluationResult.flatMessages, + }) + throw new Error("Conditions not met") + } + + logger.info({ + msg: "Conditions met at trigger time - proceeding with scheduling", + automationId, + }) + } + + const runAt = addDuration({ + duration, + interval, + }).toISOString() + + const scheduleAutomationRun = await insertAutomationRun(db, { + automationId, + actionRuns: automation.actionInstances.map((ai) => ({ + actionInstanceId: ai.id, + config: ai.config, + result: { scheduled: `Action scheduled for ${runAt}` }, + status: ActionRunStatus.scheduled, + })), + pubId, + communityId: community.id, + stack, + scheduledAutomationRunId: undefined, + trigger: { + event: AutomationEvent.pubInStageForDuration, + config: trigger.config as Record | null, + }, + userId: undefined, + }) + + const jobsClient = await getJobsClient() + + await jobsClient.scheduleDelayedAutomation({ + automationId, + pubId, + stageId: automation.stageId as StagesId, + community: { + slug: community.slug, + }, + stack, + scheduledAutomationRunId: scheduleAutomationRun.id, + duration, + interval, + trigger: { + event: AutomationEvent.pubInStageForDuration, + config: trigger.config as Record | null, + }, + }) + + return { + automationId, + runAt, + } +} + +export const cancelScheduledAutomation = async ( + automationRunId: AutomationRunsId, + communityId: CommunitiesId +): Promise<{ success: boolean; error?: string }> => { + try { + const automationRun = await getAutomationRunById( + communityId, + automationRunId + ).executeTakeFirstOrThrow() + + if (!automationRun) { + logger.warn({ + msg: "Automation run not found", + automationRunId, + }) + return { success: false, error: "Automation run not found" } + } + + const jobKey = getScheduledAutomationJobKey({ + stageId: automationRun.stage?.id as StagesId, + automationId: automationRun.automation?.id as AutomationsId, + pubId: automationRun.actionRuns[0]?.pubId as PubsId, + trigger: { + event: automationRun.triggerEvent, + config: automationRun.triggerConfig as Record | null, + }, + }) + + const jobsClient = await getJobsClient() + await jobsClient.unscheduleJob(jobKey) + + await insertAutomationRun(db, { + automationId: automationRun.automation?.id as AutomationsId, + scheduledAutomationRunId: automationRunId, + communityId, + stack: [automationRunId], + trigger: { + event: AutomationEvent.pubInStageForDuration, + config: automationRun.triggerConfig as Record | null, + }, + actionRuns: automationRun.actionRuns.map((ar) => ({ + actionInstanceId: expect(ar.actionInstanceId), + config: ar.config as BaseActionInstanceConfig, + result: { cancelled: "Automation cancelled because pub left stage" }, + status: ActionRunStatus.failure, + })), + }) + + logger.info({ + msg: "Successfully cancelled scheduled automation", + automationRunId, + jobKey, + }) + + return { success: true } + } catch (error) { + logger.error({ + msg: "Error cancelling scheduled automation", + automationRunId, + error, + }) + return { + success: false, + error: error instanceof Error ? error.message : "Unknown error", + } + } +} diff --git a/core/actions/_lib/triggers.tsx b/core/actions/_lib/triggers.tsx new file mode 100644 index 0000000000..a3f971d45b --- /dev/null +++ b/core/actions/_lib/triggers.tsx @@ -0,0 +1,267 @@ +import type { Automations, AutomationsId, Communities } from "db/public" +import type { UseFormReturn } from "react-hook-form" +import type { SequentialAutomationEvent } from "~/actions/types" + +import { + ArrowRightFromLine, + ArrowRightToLine, + CalendarClock, + CheckCircle, + Globe, + Hand, + XCircle, +} from "lucide-react" +import { z } from "zod" + +import { AutomationEvent } from "db/public" +import { CopyButton } from "ui/copy-button" + +import { defineAutomation, sequentialAutomationEvents } from "~/actions/types" + +export const intervals = ["minute", "hour", "day", "week", "month", "year"] as const +export type Interval = (typeof intervals)[number] + +export const pubInStageForDuration = defineAutomation({ + event: AutomationEvent.pubInStageForDuration, + config: z.object({ + duration: z.number().int().min(1), + interval: z.enum(intervals), + }), + display: { + icon: CalendarClock, + base: "a pub stays in this stage for...", + hydrated: ({ config: { duration, interval } }) => + `a pub stays in this stage for ${duration} ${interval}s`, + }, +}) +export type PubInStageForDuration = typeof pubInStageForDuration + +export const pubLeftStage = defineAutomation({ + event: AutomationEvent.pubLeftStage, + config: undefined, + display: { + icon: ArrowRightFromLine, + base: "a pub leaves this stage", + }, +}) +export type PubLeftStage = typeof pubLeftStage + +export const pubEnteredStage = defineAutomation({ + event: AutomationEvent.pubEnteredStage, + config: undefined, + display: { + icon: ArrowRightToLine, + base: "a pub enters this stage", + }, +}) +export type PubEnteredStage = typeof pubEnteredStage + +export const automationSucceeded = defineAutomation({ + event: AutomationEvent.automationSucceeded, + config: undefined, + display: { + icon: CheckCircle, + base: "a specific automation succeeds", + // hydrated: ({ config }) => `${config.name} succeeds`, + }, +}) +export type AutomationSucceeded = typeof automationSucceeded + +export const automationFailed = defineAutomation({ + event: AutomationEvent.automationFailed, + config: undefined, + display: { + icon: XCircle, + base: "a specific action fails", + // hydrated: ({ config }) => `${config.name} fails`, + }, +}) +export type AutomationFailed = typeof automationFailed + +export const constructWebhookUrl = (automationId: AutomationsId, communitySlug: string) => + `/api/v0/c/${communitySlug}/site/webhook/${automationId}` + +export const webhook = defineAutomation({ + event: AutomationEvent.webhook, + config: undefined, + display: { + icon: Globe, + base: ({ community }) => ( + + a request is made to{" "} + + {constructWebhookUrl("" as AutomationsId, community.slug)} + + + ), + hydrated: ({ automation, community }) => ( + + a request is made to{" "} + {constructWebhookUrl(automation.id, community.slug)} + + + ), + }, +}) +export type Webhook = typeof webhook + +export const manual = defineAutomation({ + event: AutomationEvent.manual, + config: undefined, + display: { + icon: Hand, + base: "this automation is run manually", + }, +}) +export type Manual = typeof manual + +export type Trigger = + | PubInStageForDuration + | PubLeftStage + | PubEnteredStage + | AutomationSucceeded + | AutomationFailed + | Webhook + | Manual + +export type SchedulableEvent = + | AutomationEvent.pubInStageForDuration + | AutomationEvent.automationFailed + | AutomationEvent.automationSucceeded + +export type AutomationForEvent = E extends E + ? Extract + : never + +export type SchedulableAutomation = AutomationForEvent + +export type AutomationConfig = A extends A + ? { + automationConfig: NonNullable["_input"] extends infer RC + ? undefined extends RC + ? null + : RC + : null + actionConfig: Record | null + } + : never + +export type AutomationConfigs = AutomationConfig | undefined + +export const triggers = { + [pubInStageForDuration.event]: pubInStageForDuration, + [pubEnteredStage.event]: pubEnteredStage, + [pubLeftStage.event]: pubLeftStage, + [automationSucceeded.event]: automationSucceeded, + [automationFailed.event]: automationFailed, + [webhook.event]: webhook, + [manual.event]: manual, +} as const satisfies Record + +export const getTriggerByName = (name: T) => { + return triggers[name] +} + +export const isReferentialTrigger = ( + automation: (typeof triggers)[keyof typeof triggers] +): automation is Extract => + sequentialAutomationEvents.includes(automation.event as any) + +export const humanReadableEventBase = ( + event: T, + community: Communities +) => { + const automation = getTriggerByName(event) + + if (typeof automation.display.base === "function") { + return automation.display.base({ community }) + } + + return automation.display.base +} + +export const humanReadableEventHydrated = ( + event: T, + community: Communities, + options: { + automation: Automations + config?: (typeof triggers)[T]["config"] extends undefined + ? never + : z.infer> + sourceAutomation?: Automations + } +) => { + const automationConf = getTriggerByName(event) + if (options.config && automationConf.config && automationConf.display.hydrated) { + return automationConf.display.hydrated({ + automation: options.automation, + community, + config: options.config, + }) + } + if ( + options.sourceAutomation && + isReferentialTrigger(automationConf) && + automationConf.display.hydrated + ) { + return automationConf.display.hydrated({ + automation: options.automation, + community, + sourceAutomation: options.sourceAutomation, + }) + } + + if (automationConf.display.hydrated && !automationConf.config) { + return automationConf.display.hydrated({ + automation: options.automation, + community, + config: {} as any, + }) + } + + if (typeof automationConf.display.base === "function") { + return automationConf.display.base({ community }) + } + + return automationConf.display.base +} + +export const humanReadableAutomation = < + A extends Automations & { triggers: { event: AutomationEvent }[] }, +>( + automation: A, + community: Communities, + instanceName: string, + config?: (typeof triggers)[keyof typeof triggers]["config"] extends undefined + ? never + : z.infer>, + sourceAutomation?: Automations | null +) => + `${instanceName} will run when ${humanReadableEventHydrated(automation.triggers[0].event, community, { automation: automation, config, sourceAutomation: sourceAutomation ?? undefined })}` + +export type TriggersWithConfig = { + [K in keyof typeof triggers]: undefined extends (typeof triggers)[K]["config"] ? never : K +}[keyof typeof triggers] + +export const isTriggerWithConfig = (trigger: AutomationEvent): trigger is TriggersWithConfig => { + return trigger in triggers && triggers[trigger].config !== undefined +} + +export type AdditionalConfigFormReturn = UseFormReturn<{ + triggers: Trigger extends infer T extends Trigger + ? { + event: T["event"] + config: z.infer> + }[] + : never +}> + +export type AdditionalConfigForm = React.FC<{ + form: AdditionalConfigFormReturn + idx: number +}> diff --git a/core/actions/_lib/triggers/PubInStageForDurationConfigForm.tsx b/core/actions/_lib/triggers/PubInStageForDurationConfigForm.tsx new file mode 100644 index 0000000000..0ee93ae61d --- /dev/null +++ b/core/actions/_lib/triggers/PubInStageForDurationConfigForm.tsx @@ -0,0 +1,95 @@ +import type { AdditionalConfigForm } from "../triggers" + +import { Controller } from "react-hook-form" + +import { Field, FieldError, FieldGroup, FieldLabel } from "ui/field" +import { Input } from "ui/input" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "ui/select" + +import { intervals } from "../triggers" + +export const PubInStageForDurationConfigForm: AdditionalConfigForm = (props) => { + return ( + { + return ( + + ( + + + Duration + + + p.field.onChange( + e.target.value ? Number(e.target.value) : undefined + ) + } + /> + {p.fieldState.error && ( + + {p.fieldState.error.message} + + )} + + )} + /> + + ( + + + Interval + + + {p.fieldState.error && ( + + {p.fieldState.error.message} + + )} + + )} + /> + {fieldState.error && ( + {fieldState.error.message} + )} + + ) + }} + /> + ) +} diff --git a/core/actions/_lib/triggers/forms.tsx b/core/actions/_lib/triggers/forms.tsx new file mode 100644 index 0000000000..cdcbf8595d --- /dev/null +++ b/core/actions/_lib/triggers/forms.tsx @@ -0,0 +1,27 @@ +import type { TriggersWithConfig } from "../triggers" + +import dynamic from "next/dynamic" + +import { AutomationEvent } from "db/public" +import { Skeleton } from "ui/skeleton" + +const triggerConfigForms = { + [AutomationEvent.pubInStageForDuration]: dynamic( + () => + import("./PubInStageForDurationConfigForm").then( + (mod) => mod.PubInStageForDurationConfigForm + ), + { + ssr: false, + loading: () => , + } + ), +} as const satisfies Record + +export const getTriggerConfigForm = (trigger: AutomationEvent) => { + if (!(trigger in triggerConfigForms)) { + return null + } + + return triggerConfigForms[trigger as TriggersWithConfig] +} diff --git a/core/actions/_lib/zodTypes.ts b/core/actions/_lib/zodTypes.ts index a55127184e..c40f9ec57a 100644 --- a/core/actions/_lib/zodTypes.ts +++ b/core/actions/_lib/zodTypes.ts @@ -32,27 +32,6 @@ class StringWithTokens extends z.ZodString { } } -const actionInstanceShape = { - name: z.string(), - description: z.string(), - icon: z.string(), - action: z.string(), - actionInstanceId: z.string().uuid(), -} - -export type ActionInstanceConfig = z.infer> - -// @ts-expect-error FIXME: '{ name: z.ZodString; description: z.ZodString; icon: z.ZodString; action: z.ZodString; actionInstanceId: z.ZodString; }' is assignable to the constraint of type 'T_1', but 'T_1' could be instantiated with a different subtype of constraint 'ZodRawShape'.ts(2417) -class ActionInstance extends z.ZodObject { - static create = () => - new ActionInstance({ - typeName: "ActionInstance" as z.ZodFirstPartyTypeKind.ZodObject, - shape: () => actionInstanceShape, - catchall: z.never(), - unknownKeys: "strip", - }) -} - class FieldName extends z.ZodString { static create = () => new FieldName({ @@ -88,7 +67,6 @@ class OutputMap extends z.ZodArray< export const markdown = Markdown.create export const stringWithTokens = StringWithTokens.create -export const actionInstance = ActionInstance.create export const fieldName = FieldName.create export const stage = Stage.create export const outputMap = OutputMap.create diff --git a/core/actions/api/index.ts b/core/actions/api/index.ts index 1b4ef09826..d229cbb40e 100644 --- a/core/actions/api/index.ts +++ b/core/actions/api/index.ts @@ -1,17 +1,5 @@ // shared actions between server and client -import type { ActionInstances, Automations, Communities, Event } from "db/public" -import type * as z from "zod" -import type { SequentialAutomationEvent } from "../types" - -import { - actionFailed, - actionSucceeded, - pubEnteredStage, - pubInStageForDuration, - pubLeftStage, - webhook, -} from "../_lib/automations" import * as buildJournalSite from "../buildJournalSite/action" import * as datacite from "../datacite/action" import * as email from "../email/action" @@ -19,7 +7,6 @@ import * as googleDriveImport from "../googleDriveImport/action" import * as http from "../http/action" import * as log from "../log/action" import * as move from "../move/action" -import { sequentialAutomationEvents } from "../types" export const actions = { [log.action.name]: log.action, @@ -42,88 +29,3 @@ export const getActionByName = (name: N) => { export const getActionNames = () => { return Object.keys(actions) as (keyof typeof actions)[] } - -export const automations = { - [pubInStageForDuration.event]: pubInStageForDuration, - [pubEnteredStage.event]: pubEnteredStage, - [pubLeftStage.event]: pubLeftStage, - [actionSucceeded.event]: actionSucceeded, - [actionFailed.event]: actionFailed, - [webhook.event]: webhook, -} as const satisfies Record - -export const getAutomationByName = (name: T) => { - return automations[name] -} - -export const isReferentialAutomation = ( - automation: (typeof automations)[keyof typeof automations] -): automation is Extract => - sequentialAutomationEvents.includes(automation.event as any) - -export const humanReadableEventBase = (event: T, community: Communities) => { - const automation = getAutomationByName(event) - - if (typeof automation.display.base === "function") { - return automation.display.base({ community }) - } - - return automation.display.base -} - -export const humanReadableEventHydrated = ( - event: T, - community: Communities, - options: { - automation: Automations - config?: (typeof automations)[T]["additionalConfig"] extends undefined - ? never - : z.infer> - sourceAction?: ActionInstances | null - } -) => { - const automationConf = getAutomationByName(event) - if (options.config && automationConf.additionalConfig && automationConf.display.hydrated) { - return automationConf.display.hydrated({ - automation: options.automation, - community, - config: options.config, - }) - } - if ( - options.sourceAction && - isReferentialAutomation(automationConf) && - automationConf.display.hydrated - ) { - return automationConf.display.hydrated({ - automation: options.automation, - community, - config: options.sourceAction, - }) - } - - if (automationConf.display.hydrated && !automationConf.additionalConfig) { - return automationConf.display.hydrated({ - automation: options.automation, - community, - config: {} as any, - }) - } - - if (typeof automationConf.display.base === "function") { - return automationConf.display.base({ community }) - } - - return automationConf.display.base -} - -export const humanReadableAutomation = ( - automation: A, - community: Communities, - instanceName: string, - config?: (typeof automations)[A["event"]]["additionalConfig"] extends undefined - ? never - : z.infer>, - sourceAction?: ActionInstances | null -) => - `${instanceName} will run when ${humanReadableEventHydrated(automation.event, community, { automation: automation, config, sourceAction })}` diff --git a/core/actions/api/server.ts b/core/actions/api/server.ts index 45a95a3dec..2bfe854512 100644 --- a/core/actions/api/server.ts +++ b/core/actions/api/server.ts @@ -1,2 +1,5 @@ -export { runActionInstance, runInstancesForEvent } from "../_lib/runActionInstance" -export { scheduleActionInstances } from "../_lib/scheduleActionInstance" +export { + runAutomation, + // runInstancesForEvent, +} from "../_lib/runAutomation" +export { scheduleDelayedAutomation } from "../_lib/scheduleAutomations" diff --git a/core/actions/api/serverAction.ts b/core/actions/api/serverAction.ts index 746dc29f60..fd2a06b1d9 100644 --- a/core/actions/api/serverAction.ts +++ b/core/actions/api/serverAction.ts @@ -1,24 +1,30 @@ "use server" -import type { UsersId } from "db/public" -import type { ActionInstanceRunResult, RunActionInstanceArgs } from "../_lib/runActionInstance" +import type { ActionInstancesId, UsersId } from "db/public" +import type { ActionInstanceRunResult, RunAutomationArgs } from "../_lib/runAutomation" -import { Capabilities, MembershipType } from "db/public" +import { AutomationEvent, Capabilities, MembershipType } from "db/public" import { getLoginData } from "~/lib/authentication/loginData" import { userCan } from "~/lib/authorization/capabilities" import { defineServerAction } from "~/lib/server/defineServerAction" -import { runActionInstance as runActionInstanceInner } from "../_lib/runActionInstance" +import { runAutomation } from "../_lib/runAutomation" -export const runActionInstance = defineServerAction(async function runActionInstance( - args: Omit +export const runAutomationManual = defineServerAction(async function runActionInstance( + args: Omit & { + manualActionInstancesOverrideArgs: { + [actionInstanceId: ActionInstancesId]: Record + } + skipConditionCheck?: boolean + } ): Promise { const { user } = await getLoginData() if (!user) { return { + success: false, error: "Not logged in", - stack: [], + config: {}, } } @@ -34,19 +40,60 @@ export const runActionInstance = defineServerAction(async function runActionInst if (!canRunAction) { return { + success: false, error: "Not authorized to run action", - stack: [], + config: {}, + } + } + + // verify the user has permission to skip condition checks if requested + let skipConditionCheck = false + if (args.skipConditionCheck) { + const canOverrideConditions = await userCan( + Capabilities.overrideAutomationConditions, + { type: MembershipType.community, communityId: args.communityId }, + user.id + ) + if (!canOverrideConditions) { + return { + success: false, + error: "Not authorized to skip condition checks", + config: {}, + } } + skipConditionCheck = true } const { json: _, pubId: __, ...rest } = args - const result = await runActionInstanceInner({ + const result = await runAutomation({ ...rest, userId: user.id as UsersId, stack: args.stack ?? [], + communityId: args.communityId, + manualActionInstancesOverrideArgs: args.manualActionInstancesOverrideArgs, + skipConditionCheck, ...(args.json ? { json: args.json } : { pubId: args.pubId! }), + // manual run + automationId: args.automationId, + trigger: { + event: AutomationEvent.manual, + config: null, + }, }) - return result + if (!result.success) { + return { + success: false, + error: result.error, + config: {}, + } + } + + return { + success: true, + data: result.data, + report: result.report, + config: {}, + } }) diff --git a/core/actions/buildJournalSite/action.ts b/core/actions/buildJournalSite/action.ts index dca09f2d01..3702007ee0 100644 --- a/core/actions/buildJournalSite/action.ts +++ b/core/actions/buildJournalSite/action.ts @@ -11,6 +11,7 @@ const schema = z.object({ export const action = defineAction({ name: Action.buildJournalSite, + niceName: "Build Journal Site", accepts: ["pub"], superAdminOnly: true, experimental: true, diff --git a/core/actions/buildJournalSite/run.tsx b/core/actions/buildJournalSite/run.tsx index 706e69f914..68f897b9ac 100644 --- a/core/actions/buildJournalSite/run.tsx +++ b/core/actions/buildJournalSite/run.tsx @@ -67,6 +67,7 @@ export const run = defineRun(async ({ pub, config }) => { if (buildError) { logger.error({ msg: "Failed to build journal site", buildError }) return { + success: false, title: "Failed to build journal site", error: buildError.message, } diff --git a/core/actions/datacite/action.ts b/core/actions/datacite/action.ts index e1f00cc0e6..495d10c1bc 100644 --- a/core/actions/datacite/action.ts +++ b/core/actions/datacite/action.ts @@ -8,6 +8,7 @@ import { defineAction } from "../types" export const action = defineAction({ name: Action.datacite, accepts: ["pub"], + niceName: "Deposit to DataCite", config: { schema: z.object({ doi: z.string().optional(), diff --git a/core/actions/datacite/form.tsx b/core/actions/datacite/form.tsx index 2bafbccf9a..e9be064659 100644 --- a/core/actions/datacite/form.tsx +++ b/core/actions/datacite/form.tsx @@ -1,6 +1,6 @@ +import { Checkbox } from "ui/checkbox" import { DatePicker } from "ui/date-picker" import { FieldSet } from "ui/field" -import { Input } from "ui/input" import { ActionField } from "../_lib/ActionField" @@ -32,12 +32,11 @@ export default function LogActionForm() { name="bylineContributorFlag" label="Byline Contributor Flag" render={({ field, fieldState }) => ( - )} /> diff --git a/core/actions/datacite/run.test.ts b/core/actions/datacite/run.test.ts index 96dac55306..a8e4214624 100644 --- a/core/actions/datacite/run.test.ts +++ b/core/actions/datacite/run.test.ts @@ -1,6 +1,6 @@ import type { - ActionInstancesId, ActionRunsId, + AutomationsId, CommunitiesId, PubFieldsId, PubsId, @@ -8,17 +8,16 @@ import type { PubValuesId, StagesId, } from "db/public" -import type { ClientExceptionOptions } from "~/lib/serverActions" import type { ActionPub, RunProps } from "../types" import type { action } from "./action" import { afterEach } from "node:test" import { describe, expect, it, vitest } from "vitest" -import { Action, CoreSchemaType } from "db/public" +import { CoreSchemaType } from "db/public" import { updatePub } from "~/lib/server" -import { didSucceed } from "~/lib/serverActions" +import { type ClientExceptionOptions, didSucceed } from "~/lib/serverActions" import { run } from "./run" vitest.mock("~/lib/env/env", () => { @@ -134,14 +133,20 @@ const pub = { } as ActionPub const RUN_OPTIONS: RunProps = { - actionInstance: { - id: "" as ActionInstancesId, + automation: { + id: "" as AutomationsId, name: "deposit to datacite", stageId: "" as StagesId, createdAt: new Date(), updatedAt: new Date(), - action: Action.datacite, - config: {}, + actionInstances: [], + triggers: [], + condition: null, + conditionEvaluationTiming: null, + icon: null, + communityId: "" as CommunitiesId, + description: null, + lastAutomationRun: null, }, actionRunId: "" as ActionRunsId, stageId: "" as StagesId, diff --git a/core/actions/datacite/run.ts b/core/actions/datacite/run.ts index 349ec08314..09fc96e153 100644 --- a/core/actions/datacite/run.ts +++ b/core/actions/datacite/run.ts @@ -295,6 +295,7 @@ const createPubDeposit = async (depositPayload: Payload) => { }, }) return { + success: false, title: "Failed to create DOI", error: "An error occurred while depositing the pub to DataCite.", } @@ -326,6 +327,7 @@ const updatePubDeposit = async (depositPayload: Payload) => { }, }) return { + success: false, title: "Failed to update DOI", error: "An error occurred while depositing the pub to DataCite.", } @@ -343,6 +345,7 @@ export const run = defineRun(async ({ pub, config, lastModifiedBy } catch (error) { if (error instanceof AssertionError) { return { + success: false, title: "Failed to create DataCite deposit", error: error.message, cause: undefined, @@ -381,6 +384,7 @@ export const run = defineRun(async ({ pub, config, lastModifiedBy }) } catch (error) { return { + success: false, title: "Failed to save DOI", error: "The pub was deposited to DataCite, but we were unable to update the pub's DOI in PubPub", cause: error.message, diff --git a/core/actions/email/action.tsx b/core/actions/email/action.tsx index fb2482973e..3f5c88f090 100644 --- a/core/actions/email/action.tsx +++ b/core/actions/email/action.tsx @@ -40,6 +40,7 @@ const schema = z.object({ "Someone who is a member of the community. Either this or 'Recipient Email' must be set." ), subject: stringWithTokens() + .min(1, { message: "Subject is required" }) .max(500) .describe( "The subject of the email. Tokens can be used to dynamically insert values from the pub or config." @@ -54,6 +55,7 @@ const schema = z.object({ export const action = defineAction({ accepts: ["pub"], name: Action.email, + niceName: "Send Email", config: { schema }, description: "Send an email to one or more users", icon: Mail, diff --git a/core/actions/email/form.tsx b/core/actions/email/form.tsx index d8fd93c9bc..70b21a8086 100644 --- a/core/actions/email/form.tsx +++ b/core/actions/email/form.tsx @@ -1,3 +1,5 @@ +import { useWatch } from "react-hook-form" + import { InputWithTokens, MarkdownEditor } from "ui/editors" import { FieldSet } from "ui/field" @@ -6,27 +8,11 @@ import { useActionForm } from "../_lib/ActionForm" import MemberSelectClientFetch from "./DynamicSelectFetch" export default function EmailActionForm() { - const { form } = useActionForm() - const [recipientMember, recipientEmail] = form.watch(["recipientMember", "recipientEmail"]) return (
- {!recipientMember && } - {!recipientEmail && ( - ( - - )} - /> - )} + ) } + +function RecipientAndMemberFields() { + const { form, path } = useActionForm() + const fullPath = path || "" + const recipientMember = useWatch({ + control: form.control, + name: fullPath ? `${fullPath}.recipientMember` : "recipientMember", + }) + const recipientEmail = useWatch({ + control: form.control, + name: fullPath ? `${fullPath}.recipientEmail` : "recipientEmail", + }) + + return ( + <> + {!recipientMember && } + {!recipientEmail && ( + ( + + )} + /> + )} + + ) +} diff --git a/core/actions/email/run.ts b/core/actions/email/run.ts index daae29dff4..8c0b47578e 100644 --- a/core/actions/email/run.ts +++ b/core/actions/email/run.ts @@ -150,6 +150,7 @@ export const run = defineRun( logger.error({ msg: "Failed to send email", error }) return { + success: false, title: "Failed to Send Email", error: error.message, cause: error, diff --git a/core/actions/googleDriveImport/action.ts b/core/actions/googleDriveImport/action.ts index 1914214227..3d3d35f29c 100644 --- a/core/actions/googleDriveImport/action.ts +++ b/core/actions/googleDriveImport/action.ts @@ -18,6 +18,7 @@ const sharedSchema = z.object({ export const action = defineAction({ name: Action.googleDriveImport, + niceName: "Import from Google Drive", accepts: ["pub"], description: "Import a Google Drive folder.", icon: SiGoogledrive, diff --git a/core/actions/googleDriveImport/run.ts b/core/actions/googleDriveImport/run.ts index 1ed5cfe6ba..39c52d953e 100644 --- a/core/actions/googleDriveImport/run.ts +++ b/core/actions/googleDriveImport/run.ts @@ -268,6 +268,7 @@ export const run = defineRun( logger.error(err) return { + success: false, title: "Error", error: "An error occurred while importing the pub from Google Drive.", cause: err, diff --git a/core/actions/http/action.ts b/core/actions/http/action.ts index 9cdafcee77..04e0ce3052 100644 --- a/core/actions/http/action.ts +++ b/core/actions/http/action.ts @@ -31,11 +31,12 @@ const schema = z.object({ export const action = defineAction({ name: Action.http, + niceName: "HTTP Request", + description: "Make an arbitrary HTTP request", accepts: ["json", "pub"], config: { schema, }, - description: "Make an arbitrary HTTP request", icon: Globe, experimental: true, }) diff --git a/core/actions/http/run.tsx b/core/actions/http/run.tsx index 1e3ee75a60..23c546495a 100644 --- a/core/actions/http/run.tsx +++ b/core/actions/http/run.tsx @@ -39,6 +39,7 @@ export const run = defineRun(async ({ pub, config, lastModifiedBy if (res.status !== 200) { return { + success: false, title: "Error", error: `Error ${res.status} ${res.statusText}`, } @@ -49,6 +50,7 @@ export const run = defineRun(async ({ pub, config, lastModifiedBy !res.headers.get("content-type")?.includes("application/json") ) { return { + success: false, title: "Error", error: `Expected application/json response, got ${res.headers.get("content-type")}`, } @@ -98,6 +100,7 @@ export const run = defineRun(async ({ pub, config, lastModifiedBy } catch (error) { logger.debug(error) return { + success: false, title: "Error", error: `Failed to update fields: ${error}`, cause: error, diff --git a/core/actions/log/action.ts b/core/actions/log/action.ts index d8c670ab9c..bd3e530ef6 100644 --- a/core/actions/log/action.ts +++ b/core/actions/log/action.ts @@ -7,6 +7,7 @@ import { defineAction } from "../types" export const action = defineAction({ name: Action.log, + niceName: "Log to Console", accepts: ["pub", "json"], config: { schema: z.object({ @@ -17,7 +18,7 @@ export const action = defineAction({ debounce: z.number().default(0).describe("Debounce time in milliseconds"), }), }, - description: "Log a pub to the console", + description: "Log information to the console. Useful for debugging and testing.", icon: Terminal, superAdminOnly: true, }) diff --git a/core/actions/log/run.ts b/core/actions/log/run.ts index 118b4f3a16..3034bbaf19 100644 --- a/core/actions/log/run.ts +++ b/core/actions/log/run.ts @@ -6,7 +6,7 @@ import { logger } from "logger" import { defineRun } from "../types" -export const run = defineRun(async ({ actionInstance, pub, config }) => { +export const run = defineRun(async ({ pub, config }) => { const text = config.text const debounce = config.debounce @@ -20,7 +20,7 @@ export const run = defineRun(async ({ actionInstance, pub, config return { success: true, report: `Logged out ${text || "some data"}, check your console.`, - title: `Successfully ran ${actionInstance.name}`, + title: `Successfully ran log action`, data: {}, } }) diff --git a/core/actions/move/action.ts b/core/actions/move/action.ts index b8653f100a..c3d2ad13eb 100644 --- a/core/actions/move/action.ts +++ b/core/actions/move/action.ts @@ -8,6 +8,7 @@ import { defineAction } from "../types" export const action = defineAction({ name: Action.move, + niceName: "Move Pub", accepts: ["pub"], config: { schema: z.object({ diff --git a/core/actions/move/run.ts b/core/actions/move/run.ts index 7fd5846b54..e2ff552bfb 100644 --- a/core/actions/move/run.ts +++ b/core/actions/move/run.ts @@ -24,6 +24,7 @@ export const run = defineRun(async ({ pub, config }) => { } logger.error({ msg: "move", error }) return { + success: false, title: "Failed to move pub", error: "An error occured while moving the pub", cause: error, diff --git a/core/actions/results.ts b/core/actions/results.ts new file mode 100644 index 0000000000..505f78cd93 --- /dev/null +++ b/core/actions/results.ts @@ -0,0 +1,104 @@ +import type { ActionRuns, AutomationRuns, AutomationRunsId } from "db/public" +import type { BaseActionInstanceConfig } from "db/types" +import type React from "react" + +import { ActionRunStatus } from "db/public" + +export type ActionRunResultSuccess = { + success: true + title?: string + report?: React.ReactNode | string + data: unknown + config: BaseActionInstanceConfig +} + +export type ActionRunResultFailure = { + success: false + title?: string + error: string + cause?: unknown + config: BaseActionInstanceConfig +} + +export type ActionRunResult = ActionRunResultSuccess | ActionRunResultFailure + +export const isActionSuccess = (result: unknown): result is ActionRunResultSuccess => { + return ( + typeof result === "object" && + result !== null && + "success" in result && + result.success === true + ) +} + +export const isActionFailure = (result: unknown): result is ActionRunResultFailure => { + return ( + typeof result === "object" && + result !== null && + "success" in result && + result.success === false + ) +} + +export type AutomationRunResultSuccess = { + success: true + title?: string + report?: React.ReactNode | string + data: unknown + actionRuns: ActionRunResult[] + stack: AutomationRunsId[] +} + +export type AutomationRunResultFailure = { + success: false + title?: string + error: string + cause?: unknown + actionRuns: ActionRunResult[] + stack: AutomationRunsId[] +} + +export type AutomationRunResult = AutomationRunResultSuccess | AutomationRunResultFailure + +export type AutomationRunComputedStatus = ActionRunStatus | "partial" + +export type AutomationRunWithStatus = AutomationRuns & { + actionRuns: ActionRuns[] + status: AutomationRunComputedStatus +} + +export const getAutomationRunStatus = ( + automationRun: AutomationRuns & { actionRuns: ActionRuns[] } +): AutomationRunComputedStatus => { + if (!automationRun.actionRuns || automationRun.actionRuns.length === 0) { + return ActionRunStatus.scheduled + } + + const statuses = automationRun.actionRuns.map((ar) => ar.status) + + if (statuses.every((s) => s === ActionRunStatus.success)) { + return ActionRunStatus.success + } + + if (statuses.every((s) => s === ActionRunStatus.failure)) { + return ActionRunStatus.failure + } + + if (statuses.every((s) => s === ActionRunStatus.scheduled)) { + return ActionRunStatus.scheduled + } + + return "partial" +} + +export const getActionRunStatusFromResult = (result: unknown): ActionRunStatus => { + if (isActionSuccess(result)) { + return ActionRunStatus.success + } + + if (isActionFailure(result)) { + return ActionRunStatus.failure + } + + return ActionRunStatus.scheduled +} diff --git a/core/actions/types.ts b/core/actions/types.ts index a6e69b079d..ccac418136 100644 --- a/core/actions/types.ts +++ b/core/actions/types.ts @@ -1,6 +1,6 @@ -import type { Json, ProcessedPub } from "contracts" +import type { Prettify } from "@ts-rest/core" +import type { ProcessedPub } from "contracts" import type { - ActionInstances, Action as ActionName, ActionRunsId, Automations, @@ -9,14 +9,15 @@ import type { StagesId, UsersId, } from "db/public" -import type { LastModifiedBy } from "db/types" +import type { FullAutomation, Json, LastModifiedBy } from "db/types" +import type React from "react" import type { Dependency, FieldConfig, FieldConfigItem } from "ui/auto-form" import type * as Icons from "ui/icon" -import type { Prettify, XOR } from "utils/types" -import type * as z from "zod" +import type { XOR } from "utils/types" +import type z from "zod" import type { ClientExceptionOptions } from "~/lib/serverActions" -import { Event } from "db/public" +import { AutomationEvent } from "db/public" export type ActionPub = ProcessedPub<{ withPubType: true @@ -44,7 +45,7 @@ export type RunProps = T extends Action< * The user ID of the user who initiated the action, if any */ userId?: UsersId - actionInstance: ActionInstances + automation: FullAutomation } & ("pub" | "json" extends Acc[number] // if only one's accepted, it's only that one // if both are accepted, it's one or the other. ? XOR<{ pub: ActionPub }, { json: Json }> : ("pub" extends Acc[number] @@ -76,6 +77,7 @@ export type Action< > = { id?: string name: N + niceName: string description: string accepts: Accepts /** @@ -136,29 +138,30 @@ export const defineRun = ( export type Run = ReturnType -export const sequentialAutomationEvents = [Event.actionSucceeded, Event.actionFailed] as const +export const sequentialAutomationEvents = [ + AutomationEvent.automationSucceeded, + AutomationEvent.automationFailed, +] as const export type SequentialAutomationEvent = (typeof sequentialAutomationEvents)[number] -export const isSequentialAutomationEvent = (event: Event): event is SequentialAutomationEvent => - sequentialAutomationEvents.includes(event as any) +export const isSequentialAutomationEvent = ( + event: AutomationEvent +): event is SequentialAutomationEvent => sequentialAutomationEvents.includes(event as any) -export const schedulableAutomationEvents = [ - Event.pubInStageForDuration, - Event.actionFailed, - Event.actionSucceeded, -] as const +export const schedulableAutomationEvents = [AutomationEvent.pubInStageForDuration] as const export type SchedulableAutomationEvent = (typeof schedulableAutomationEvents)[number] -export const isSchedulableAutomationEvent = (event: Event): event is SchedulableAutomationEvent => - schedulableAutomationEvents.includes(event as any) +export const isSchedulableAutomationEvent = ( + event: AutomationEvent +): event is SchedulableAutomationEvent => schedulableAutomationEvents.includes(event as any) export type EventAutomationOptionsBase< - E extends Event, + E extends AutomationEvent, AC extends Record | undefined = undefined, > = { event: E canBeRunAfterAddingAutomation?: boolean - additionalConfig?: AC extends Record ? z.ZodType : undefined + config: undefined extends AC ? undefined : z.ZodType /** * The display name options for this event */ @@ -173,26 +176,31 @@ export type EventAutomationOptionsBase< * Useful if you want to show some configuration or automation-specific information */ hydrated?: ( - options: { - automation: Automations - community: Communities - } & (AC extends Record - ? { config: AC } - : E extends SequentialAutomationEvent - ? { config: ActionInstances } - : {}) + options: NonNullable extends undefined + ? { + automation: Automations + community: Communities + sourceAutomation?: Automations + config?: never + } + : { + automation: Automations + community: Communities + sourceAutomation?: Automations + config: NonNullable + } ) => React.ReactNode } } export const defineAutomation = < - E extends Event, + E extends AutomationEvent, AC extends Record | undefined = undefined, >( options: EventAutomationOptionsBase ) => options -export type { AutomationConfig, AutomationConfigs } from "./_lib/automations" +export type { AutomationConfig, AutomationConfigs } from "./_lib/triggers" export type ConfigOf = T extends Action ? z.infer : never diff --git a/core/app/(user)/settings/UserInfoForm.tsx b/core/app/(user)/settings/UserInfoForm.tsx index f28450fa45..81f6113078 100644 --- a/core/app/(user)/settings/UserInfoForm.tsx +++ b/core/app/(user)/settings/UserInfoForm.tsx @@ -72,13 +72,13 @@ export function UserInfoForm({ user }: { user: UserLoginData }) { diff --git a/core/app/api/v0/c/[communitySlug]/internal/[...ts-rest]/route.ts b/core/app/api/v0/c/[communitySlug]/internal/[...ts-rest]/route.ts index 8d16bd0d6b..81497a9ada 100644 --- a/core/app/api/v0/c/[communitySlug]/internal/[...ts-rest]/route.ts +++ b/core/app/api/v0/c/[communitySlug]/internal/[...ts-rest]/route.ts @@ -1,14 +1,16 @@ -import type { ActionInstancesId, CommunitiesId, PubsId, StagesId } from "db/public" +import type { AutomationRunsId, AutomationsId, CommunitiesId, PubsId } from "db/public" import { createNextHandler } from "@ts-rest/serverless/next" import { api } from "contracts" -import { Event } from "db/public" +import { AutomationEvent } from "db/public" import { logger } from "logger" -import { runInstancesForEvent } from "~/actions/_lib/runActionInstance" -import { scheduleActionInstances } from "~/actions/_lib/scheduleActionInstance" -import { runActionInstance } from "~/actions/api/server" +import { runAutomation } from "~/actions/_lib/runAutomation" +import { + cancelScheduledAutomation, + scheduleDelayedAutomation, +} from "~/actions/_lib/scheduleAutomations" import { compareAPIKeys, getBearerToken } from "~/lib/authentication/api" import { env } from "~/lib/env/env" import { NotFoundError, tsRestHandleErrors } from "~/lib/server" @@ -22,83 +24,167 @@ const checkAuthentication = (authHeader: string) => { const handler = createNextHandler( api.internal, { - triggerAction: async ({ headers, params, body }) => { + runAutomation: async ({ headers, params, body }) => { checkAuthentication(headers.authorization) - const { event, stack, scheduledActionRunId, config, ...rest } = body + const { automationId } = params + const { pubId, trigger, stack } = body - const { actionInstanceId } = params const community = await findCommunityBySlug() if (!community) { throw new NotFoundError("Community not found") } logger.info({ - msg: "Triggering action", - actionInstanceId, - communityId: community.id, + msg: "Running automation", + automationId, + pubId, + trigger, stack, - scheduledActionRunId, - config, - ...rest, + communityId: community.id, }) - const actionRunResults = await runActionInstance({ - event: event, - actionInstanceId: actionInstanceId as ActionInstancesId, + + const result = await runAutomation({ + automationId: automationId as AutomationsId, + manualActionInstancesOverrideArgs: null, + pubId: pubId as PubsId, + trigger: { + event: trigger.event as AutomationEvent, + config: trigger.config as Record | null, + }, communityId: community.id as CommunitiesId, - stack: stack ?? [], - scheduledActionRunId: scheduledActionRunId, - actionInstanceArgs: config ?? null, - ...rest, + stack: stack as unknown as AutomationRunsId[], }) return { status: 200, - body: { result: actionRunResults, actionInstanceId }, + body: { + automationId, + result: result, + }, } }, - triggerActions: async ({ headers, params, body }) => { + scheduleDelayedAutomation: async ({ headers, params, body }) => { checkAuthentication(headers.authorization) - const { event, pubId } = body - - const { stageId } = params + const { automationId } = params + const { pubId, stack } = body const community = await findCommunityBySlug() if (!community) { throw new NotFoundError("Community not found") } - const actionRunResults = await runInstancesForEvent( - pubId as PubsId, - stageId as StagesId, - event as Event, - community.id as CommunitiesId, - [] - ) + logger.info({ + msg: "Scheduling delayed automation", + automationId, + pubId, + stack, + communityId: community.id, + }) + + const result = await scheduleDelayedAutomation({ + automationId: automationId as AutomationsId, + pubId: pubId as PubsId, + stack: stack as unknown as AutomationRunsId[], + }) return { status: 200, - body: actionRunResults, + body: result, } }, - scheduleAction: async ({ headers, params, body }) => { + runDelayedAutomation: async ({ headers, params, body }) => { checkAuthentication(headers.authorization) - const { pubId } = body - const { stageId } = params + const { automationId } = params + const { pubId, trigger, stack, automationRunId } = body const community = await findCommunityBySlug() if (!community) { throw new NotFoundError("Community not found") } - const actionScheduleResults = await scheduleActionInstances({ + logger.info({ + msg: "Running delayed automation", + automationId, + pubId, + trigger, + scheduledAutomationRunId: automationRunId, + stack: stack as unknown as AutomationRunsId[], + communityId: community.id, + }) + + const result = await runAutomation({ + automationId: automationId as AutomationsId, + manualActionInstancesOverrideArgs: null, pubId: pubId as PubsId, - stageId: stageId as StagesId, - stack: [], - event: Event.pubInStageForDuration, + trigger, + communityId: community.id as CommunitiesId, + stack: stack as unknown as AutomationRunsId[], + scheduledAutomationRunId: automationRunId, + }) + + return { + status: 200, + body: { + automationId, + result: result, + }, + } + }, + cancelScheduledAutomation: async ({ headers, params }) => { + checkAuthentication(headers.authorization) + const { automationRunId } = params + + const community = await findCommunityBySlug() + if (!community) { + throw new NotFoundError("Community not found") + } + + const result = await cancelScheduledAutomation( + automationRunId, + community.id as CommunitiesId + ) + + return { + status: result.success ? 200 : 400, + body: { + success: result.success, + }, + } + }, + runWebhookAutomation: async ({ headers, params, body }) => { + checkAuthentication(headers.authorization) + const { automationId } = params + const { json, stack } = body + + const community = await findCommunityBySlug() + if (!community) { + throw new NotFoundError("Community not found") + } + + logger.info({ + msg: "Running webhook automation", + automationId, + stack, + communityId: community.id, + }) + + const result = await runAutomation({ + automationId: automationId as AutomationsId, + json: json as any, + trigger: { + event: AutomationEvent.webhook, + config: null, + }, + manualActionInstancesOverrideArgs: null, + communityId: community.id as CommunitiesId, + stack: stack as unknown as AutomationRunsId[], }) return { status: 200, - body: actionScheduleResults ?? [], + body: { + automationId, + result: result, + }, } }, }, diff --git a/core/app/api/v0/c/[communitySlug]/site/[...ts-rest]/route.ts b/core/app/api/v0/c/[communitySlug]/site/[...ts-rest]/route.ts index d605b4a670..20e3f8a12f 100644 --- a/core/app/api/v0/c/[communitySlug]/site/[...ts-rest]/route.ts +++ b/core/app/api/v0/c/[communitySlug]/site/[...ts-rest]/route.ts @@ -13,15 +13,15 @@ import { siteApi, TOTAL_PUBS_COUNT_HEADER } from "contracts" import { ApiAccessScope, ApiAccessType, + AutomationEvent, Capabilities, ElementType, - Event, InputComponent, MembershipType, } from "db/public" import { logger } from "logger" -import { scheduleActionInstances } from "~/actions/api/server" +import { runAutomation } from "~/actions/_lib/runAutomation" import { checkAuthorization, getAuthorization, @@ -29,7 +29,7 @@ import { shouldReturnRepresentation, } from "~/lib/authentication/api" import { userHasAccessToForm } from "~/lib/authorization/capabilities" -import { getStage } from "~/lib/db/queries" +import { getAutomation, getStage } from "~/lib/db/queries" import { BadRequestError, createPubRecursiveNew, @@ -39,6 +39,8 @@ import { fullTextSearch, getPubsCount, getPubsWithRelatedValues, + getPubType, + getPubTypesForCommunity, NotFoundError, removeAllPubRelationsBySlugs, removePubRelations, @@ -47,11 +49,9 @@ import { updatePub, upsertPubRelations, } from "~/lib/server" -import { getAutomation } from "~/lib/server/automations" import { findCommunityBySlug } from "~/lib/server/community" import { getForm } from "~/lib/server/form" import { validateFilter } from "~/lib/server/pub-filters-validate" -import { getPubType, getPubTypesForCommunity } from "~/lib/server/pubtype" import { getStages } from "~/lib/server/stages" import { getMember, getSuggestedUsers } from "~/lib/server/user" @@ -735,7 +735,7 @@ const handler = createNextHandler( const automationId = params.automationId as AutomationsId - const automation = await getAutomation(automationId, community.id).executeTakeFirst() + const automation = await getAutomation(automationId) if (!automation) { throw new NotFoundError(`Automation ${automationId} not found`) @@ -748,12 +748,16 @@ const handler = createNextHandler( } try { - await scheduleActionInstances({ - event: Event.webhook, - stack: [], - stageId: automation.actionInstance.stageId, + await runAutomation({ + automationId, json: body, - config: automation.config?.actionConfig, + trigger: { + event: AutomationEvent.webhook, + config: null, + }, + manualActionInstancesOverrideArgs: null, + communityId: community.id as CommunitiesId, + stack: [], }) return { diff --git a/core/app/api/v0/c/[communitySlug]/sse/route.ts b/core/app/api/v0/c/[communitySlug]/sse/route.ts index 07224f42b7..5959ccfcd6 100644 --- a/core/app/api/v0/c/[communitySlug]/sse/route.ts +++ b/core/app/api/v0/c/[communitySlug]/sse/route.ts @@ -16,7 +16,7 @@ type Tables = (typeof databaseTableNames)[number] /** * Tables that are currently supported for SSE notifications */ -const notifyTables = ["action_runs"] as const satisfies Tables[] +const notifyTables = ["automation_runs"] as const satisfies Tables[] export type NotifyTables = (typeof notifyTables)[number] const parseNotifyTables = (tables: string[]): NotifyTables[] => { @@ -51,7 +51,7 @@ export const GET = (req: NextRequest) => { let timeoutId: NodeJS.Timeout | undefined const cleanup = async () => { - logger.info({ connectionId, msg: "closing sse connection" }) + logger.debug({ connectionId, msg: "closing sse connection" }) if (interval) { clearInterval(interval) @@ -74,7 +74,7 @@ export const GET = (req: NextRequest) => { } try { - logger.info({ connectionId, msg: "releasing client" }) + logger.debug({ connectionId, msg: "releasing client" }) client.release() } catch (err) { logger.error({ connectionId, msg: "error releasing client", err }) @@ -88,7 +88,7 @@ export const GET = (req: NextRequest) => { onClose(cleanup) if (!listen?.length) { - logger.info({ + logger.debug({ msg: "no listen tables, closing sse connection", connectionId, }) @@ -96,7 +96,7 @@ export const GET = (req: NextRequest) => { return } - logger.info({ connectionId, msg: "opening sse connection" }) + logger.debug({ connectionId, msg: "opening sse connection" }) try { const [{ user }, community] = await Promise.all([getLoginData(), findCommunityBySlug()]) @@ -124,7 +124,7 @@ export const GET = (req: NextRequest) => { // setup heartbeat interval interval = setInterval(() => { - logger.info({ connectionId, msg: "sending heartbeat" }) + logger.debug({ connectionId, msg: "sending heartbeat" }) send("heartbeat", connectionId) }, HEARTBEAT_INTERVAL) @@ -146,7 +146,7 @@ export const GET = (req: NextRequest) => { return } - logger.info({ + logger.debug({ connectionId, msg: "notification", notification, @@ -167,7 +167,7 @@ export const GET = (req: NextRequest) => { // setup max idle timeout timeoutId = setTimeout(async () => { - logger.info({ + logger.debug({ connectionId, msg: "closing sse connection after max idle time", userId: user.id, diff --git a/core/app/c/[communitySlug]/ContentLayout.tsx b/core/app/c/[communitySlug]/ContentLayout.tsx index 768546909a..3d4b2c2667 100644 --- a/core/app/c/[communitySlug]/ContentLayout.tsx +++ b/core/app/c/[communitySlug]/ContentLayout.tsx @@ -42,7 +42,9 @@ export const ContentLayout = ({
-
{children}
+
+ {children} +
) diff --git a/core/app/c/[communitySlug]/SideNav.tsx b/core/app/c/[communitySlug]/SideNav.tsx index 7861608d5b..a0947691f2 100644 --- a/core/app/c/[communitySlug]/SideNav.tsx +++ b/core/app/c/[communitySlug]/SideNav.tsx @@ -7,9 +7,9 @@ import { cache, Suspense } from "react" import { Capabilities, MembershipType } from "db/public" import { - Activity, BookOpen, BookOpenText, + Bot, CurlyBraces, FlagTriangleRightIcon, Form, @@ -103,9 +103,9 @@ const viewLinks: LinkGroupDefinition = { authorization: userCanViewStagePage, }, { - href: "/activity/actions", - text: "Action Log", - icon: , + href: "/activity/automations", + text: "Automation Log", + icon: , authorization: userCanEditCommunityCached, }, ], diff --git a/core/app/c/[communitySlug]/_components/FilterPopover.tsx b/core/app/c/[communitySlug]/_components/FilterPopover.tsx new file mode 100644 index 0000000000..8c3ac11377 --- /dev/null +++ b/core/app/c/[communitySlug]/_components/FilterPopover.tsx @@ -0,0 +1,37 @@ +"use client" + +import type { ReactNode } from "react" + +import { useState } from "react" +import { Filter } from "lucide-react" + +import { Button } from "ui/button" +import { Popover, PopoverContent, PopoverTrigger } from "ui/popover" + +type FilterPopoverProps = { + activeFilterCount: number + children: ReactNode +} + +export const FilterPopover = ({ activeFilterCount, children }: FilterPopoverProps) => { + const [open, setOpen] = useState(false) + + return ( + + + + + +
{children}
+
+
+ ) +} diff --git a/core/app/c/[communitySlug]/_components/SearchBar.tsx b/core/app/c/[communitySlug]/_components/SearchBar.tsx new file mode 100644 index 0000000000..a5dd44d5b2 --- /dev/null +++ b/core/app/c/[communitySlug]/_components/SearchBar.tsx @@ -0,0 +1,93 @@ +"use client" + +import type { ReactNode } from "react" + +import { useRef } from "react" +import { Search, X } from "lucide-react" + +import { KeyboardShortcutPriority, useKeyboardShortcut, usePlatformModifierKey } from "ui/hooks" +import { Input } from "ui/input" +import { cn } from "utils" + +type SearchBarProps = { + value: string + onChange: (value: string) => void + placeholder?: string + className?: string + actions?: ReactNode +} + +export const SearchBar = ({ + value, + onChange, + placeholder = "Search...", + className, + actions, +}: SearchBarProps) => { + const inputRef = useRef(null) + const { symbol, platform } = usePlatformModifierKey() + + useKeyboardShortcut( + "Mod+k", + () => { + inputRef.current?.focus() + inputRef.current?.select() + }, + { + priority: KeyboardShortcutPriority.MEDIUM, + } + ) + + const handleClear = () => { + onChange("") + } + + return ( +
+
+ + onChange(e.target.value)} + placeholder={placeholder} + className={cn("bg-white pl-8 tracking-wide shadow-none", value && "pr-8")} + /> + + {value && ( + + )} + + + {symbol} + {" "} + K + + +
+ {actions} +
+ ) +} diff --git a/core/app/c/[communitySlug]/_components/SortDropdown.tsx b/core/app/c/[communitySlug]/_components/SortDropdown.tsx new file mode 100644 index 0000000000..4ebbc5987c --- /dev/null +++ b/core/app/c/[communitySlug]/_components/SortDropdown.tsx @@ -0,0 +1,60 @@ +"use client" + +import { ArrowUpDownIcon, SortAsc, SortDesc } from "lucide-react" + +import { Button } from "ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "ui/dropdown-menu" + +type SortOption = { + id: string + label: string +} + +type SortDropdownProps = { + options: SortOption[] + currentSort?: { id: string; desc: boolean } + onSortChange: (sortId: string, desc: boolean) => void +} + +export const SortDropdown = ({ options, currentSort, onSortChange }: SortDropdownProps) => { + const currentOption = options.find((opt) => opt.id === currentSort?.id) + + return ( + + + + + + {options.map((option) => ( + + onSortChange( + option.id, + currentSort?.id === option.id ? !currentSort.desc : true + ) + } + > + {option.label} + + ))} + + + ) +} diff --git a/core/app/c/[communitySlug]/activity/actions/ActionRunsTable.tsx b/core/app/c/[communitySlug]/activity/actions/ActionRunsTable.tsx deleted file mode 100644 index 28d0e032ce..0000000000 --- a/core/app/c/[communitySlug]/activity/actions/ActionRunsTable.tsx +++ /dev/null @@ -1,17 +0,0 @@ -"use client" - -import type { ActionRun } from "./getActionRunsTableColumns" - -import { DataTable } from "~/app/components/DataTable/DataTable" -import { getActionRunsTableColumns } from "./getActionRunsTableColumns" - -export const ActionRunsTable = ({ - actionRuns, - communitySlug, -}: { - actionRuns: ActionRun[] - communitySlug: string -}) => { - const actionRunsColumns = getActionRunsTableColumns(communitySlug) - return -} diff --git a/core/app/c/[communitySlug]/activity/actions/getActionRunsTableColumns.tsx b/core/app/c/[communitySlug]/activity/actions/getActionRunsTableColumns.tsx deleted file mode 100644 index 65df6ec79d..0000000000 --- a/core/app/c/[communitySlug]/activity/actions/getActionRunsTableColumns.tsx +++ /dev/null @@ -1,146 +0,0 @@ -"use client" - -import type { ColumnDef } from "@tanstack/react-table" -import type { Json } from "contracts" -import type { ActionInstances, PubsId, Stages } from "db/public" -import type { Writeable, XOR } from "utils/types" -import type { PubTitleProps } from "~/lib/pubs" - -import Link from "next/link" - -import { Event } from "db/public" -import { Badge } from "ui/badge" -import { DataTableColumnHeader } from "ui/data-table" -import { HoverCard, HoverCardContent, HoverCardTrigger } from "ui/hover-card" - -import { PubTitle } from "~/app/components/PubTitle" -import { getPubTitle } from "~/lib/pubs" - -export type ActionRun = { - id: string - createdAt: Date - actionInstance: { name: string; action: string } | null - sourceActionInstance: { name: string; action: string } | null - stage: { id: string; name: string } | null - result: unknown -} & ( - | { - event: Event - user: null - } - | { - event: null - user: { - id: string - firstName: string | null - lastName: string | null - } - } -) & - XOR<{ pub: PubTitleProps & { id: PubsId } }, { json: Json }> - -export const getActionRunsTableColumns = (communitySlug: string) => { - const cols = [ - { - header: ({ column }) => , - accessorKey: "actionInstance", - cell: ({ getValue }) => { - const actionInstance = getValue() - return actionInstance ? actionInstance.name : "Unknown" - }, - } satisfies ColumnDef, - { - header: ({ column }) => , - accessorKey: "event", - cell: ({ getValue, row }) => { - const user = row.original.user - if (user) { - return `${user.firstName} ${user.lastName}` - } - switch (getValue()) { - case Event.actionFailed: - return `Automation (${row.original.sourceActionInstance?.name} failed)` - case Event.actionSucceeded: - return `Automation (${row.original.sourceActionInstance?.name} succeeded)` - case Event.pubEnteredStage: - return "Automation (Pub entered stage)" - case Event.pubLeftStage: - return "Automation (Pub exited stage)" - case Event.pubInStageForDuration: - return "Automation (Pub in stage for duration)" - case Event.webhook: - return "Automation (Webhook)" - } - }, - } satisfies ColumnDef, - { - header: ({ column }) => , - accessorKey: "stage", - cell: ({ getValue }) => { - const stage = getValue() - return stage ? stage.name : "Unknown" - }, - } satisfies ColumnDef, - { - id: "input", - accessorFn: (row) => - row.pub ? getPubTitle(row.pub) : (JSON.stringify(row.json, null, 2) ?? "unknown"), - header: ({ column }) => , - cell: ({ row }) => { - return row.original.pub ? ( - - - - ) : ( -
-						{JSON.stringify(row.original.json, null, 2)}
-					
- ) - }, - } satisfies ColumnDef, - { - header: ({ column }) => , - accessorKey: "createdAt", - } satisfies ColumnDef, - { - header: ({ column }) => , - accessorKey: "status", - cell: ({ row, getValue }) => { - let badge: React.ReactNode - switch (getValue()) { - case "success": - badge = success - break - case "failure": - badge = failure - break - case "scheduled": - badge = ( - - scheduled - - ) - break - default: - badge = unknown - break - } - return ( - - {badge} - -
-								{JSON.stringify(row.original.result, null, 2)}
-							
-
-
- ) - }, - } satisfies ColumnDef, - ] as const // satisfies ColumnDef[]; - - return cols as Writeable -} diff --git a/core/app/c/[communitySlug]/activity/automations/AutomationRunCard.tsx b/core/app/c/[communitySlug]/activity/automations/AutomationRunCard.tsx new file mode 100644 index 0000000000..8c1c12d065 --- /dev/null +++ b/core/app/c/[communitySlug]/activity/automations/AutomationRunCard.tsx @@ -0,0 +1,215 @@ +"use client" + +import type { Action, ActionRuns, AutomationRuns, Pubs } from "db/public" +import type { IconConfig } from "ui/dynamic-icon" + +import Link from "next/link" +import { User, Zap } from "lucide-react" + +import { AutomationEvent } from "db/public" +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "ui/accordion" +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "ui/collapsible" +import { DynamicIcon } from "ui/dynamic-icon" + +import { actions } from "~/actions/api" +import { getAutomationRunStatus } from "~/actions/results" +import { ActionRunResult } from "~/app/components/AutomationUI/ActionRunResult" +import { AutomationRunStatusBadge } from "~/app/components/AutomationUI/AutomationRunResult" +import { EllipsisMenu, EllipsisMenuButton } from "~/app/components/EllipsisMenu" +import { PubCardClient } from "~/app/components/pubs/PubCard/PubCardClient" +import { formatDateAsPossiblyDistance } from "~/lib/dates" + +type AutomationRunCardProps = { + automationRun: AutomationRuns & { + inputPub: Pubs | null + actionRuns: (ActionRuns & { + pubId: string | null + pubTitle: string | null + json: unknown + })[] + automation: { + id: string + name: string + icon: IconConfig | null + } | null + user: { + id: string + firstName: string | null + lastName: string | null + } | null + stage: { + id: string + name: string + } | null + } + communitySlug: string +} + +const getTriggerDescription = (automationRun: AutomationRunCardProps["automationRun"]): string => { + if (automationRun.user) { + return `${automationRun.user.firstName} ${automationRun.user.lastName}` + } + + switch (automationRun.triggerEvent) { + case AutomationEvent.automationFailed: + return "Automation failed" + case AutomationEvent.automationSucceeded: + return "Automation succeeded" + case AutomationEvent.pubEnteredStage: + return `Pub entered stage${automationRun.stage ? `: ${automationRun.stage.name}` : ""}` + case AutomationEvent.pubLeftStage: + return `Pub left stage${automationRun.stage ? `: ${automationRun.stage.name}` : ""}` + case AutomationEvent.pubInStageForDuration: + return `Pub in stage for duration${automationRun.stage ? `: ${automationRun.stage.name}` : ""}` + case AutomationEvent.webhook: + return "Webhook" + case AutomationEvent.manual: + return "Manual trigger" + default: + return "Unknown trigger" + } +} + +const getInputDescription = ( + automationRun: AutomationRunCardProps["automationRun"], + communitySlug: string +): React.ReactNode => { + if (automationRun.inputPub) { + return ( + + ) + } + + if (automationRun.inputJson) { + const jsonString = JSON.stringify(automationRun.inputJson) + const preview = jsonString.length > 50 ? `${jsonString.slice(0, 50)}...` : jsonString + return {preview} + } + + return No input +} + +export const AutomationRunCard = ({ automationRun, communitySlug }: AutomationRunCardProps) => { + const status = getAutomationRunStatus(automationRun) + const triggerDescription = getTriggerDescription(automationRun) + const inputDescription = getInputDescription(automationRun, communitySlug) + + return ( +
+ {/* Header - spans both columns on large screens */} + +
+ {automationRun.automation?.icon ? ( + + ) : ( + + )} +
+
+
+
+

+ {automationRun.automation?.name || "Unknown Automation"} +

+ +
+ + + + View Stage + + + + + Edit Stage + + + + + Edit Automation + + + +
+
+
+ + {triggerDescription} + · + +
+ + Show details + +
+
+ + + {/* Actions - left side */} + + {automationRun.actionRuns.length > 0 && ( +
+ + {automationRun.actionRuns.map((actionRun) => { + const action = actions[actionRun.action as Action] + return ( + + +
+ {action && } + + {action?.niceName || "Unknown Action"} + + {actionRun.status && ( + + {actionRun.status} + + )} +
+
+ + + +
+ ) + })} +
+
+ )} + + {/* Input - right side on desktop, below on mobile */} +
+ {inputDescription} +
+
+
+
+ ) +} diff --git a/core/app/c/[communitySlug]/activity/automations/AutomationRunClearSearchButton.tsx b/core/app/c/[communitySlug]/activity/automations/AutomationRunClearSearchButton.tsx new file mode 100644 index 0000000000..845926250a --- /dev/null +++ b/core/app/c/[communitySlug]/activity/automations/AutomationRunClearSearchButton.tsx @@ -0,0 +1,30 @@ +"use client" + +import { Button } from "ui/button" + +import { useAutomationRunSearch } from "./AutomationRunSearchProvider" + +export const AutomationRunClearSearchButton = () => { + const { setFilters } = useAutomationRunSearch() + + return ( + + ) +} diff --git a/core/app/c/[communitySlug]/activity/automations/AutomationRunList.tsx b/core/app/c/[communitySlug]/activity/automations/AutomationRunList.tsx new file mode 100644 index 0000000000..ed1e435e98 --- /dev/null +++ b/core/app/c/[communitySlug]/activity/automations/AutomationRunList.tsx @@ -0,0 +1,211 @@ +import type { AutoReturnType } from "~/lib/types" + +import { Suspense } from "react" +import { Activity } from "lucide-react" + +import { Action, type CommunitiesId } from "db/public" +import { + Empty, + EmptyContent, + EmptyDescription, + EmptyHeader, + EmptyMedia, + EmptyTitle, +} from "ui/empty" +import { cn } from "utils" + +import { db } from "~/kysely/database" +import { getAutomationRuns, getAutomationRunsCount } from "~/lib/server/actions" +import { autoCache } from "~/lib/server/cache/autoCache" +import { getCommunitySlug } from "~/lib/server/cache/getCommunitySlug" +import { getStages } from "~/lib/server/stages" +import { AutomationRunCard } from "./AutomationRunCard" +import { AutomationRunListSkeleton } from "./AutomationRunListSkeleton" +import { AutomationRunSearchFooter } from "./AutomationRunSearchFooter" +import { AutomationRunSearch } from "./AutomationRunSearchInput" +import { AutomationRunSearchProvider } from "./AutomationRunSearchProvider" +import { + automationRunSearchParamsCache, + getAutomationRunFilterParamsFromSearch, +} from "./automationRunQuery" + +type PaginatedAutomationRunListProps = { + communityId: CommunitiesId + searchParams: { [key: string]: string | string[] | undefined } + basePath?: string +} + +const PaginatedAutomationRunListInner = async ( + props: PaginatedAutomationRunListProps & { + communitySlug: string + automationRunsPromise: Promise["execute"]> + filterParams: { + statuses?: string[] + actions?: string[] + } + } +) => { + let automationRuns = await props.automationRunsPromise + + // client-side filtering for status (since we need to compute it from actionRuns) + if (props.filterParams.statuses && props.filterParams.statuses.length > 0) { + automationRuns = automationRuns.filter((run) => { + const status = getAutomationRunStatus(run) + return props.filterParams.statuses?.includes(status) + }) + } + + // client-side filtering for actions + if (props.filterParams.actions && props.filterParams.actions.length > 0) { + automationRuns = automationRuns.filter((run) => { + return run.actionRuns.some((actionRun) => + props.filterParams.actions?.includes(actionRun.action ?? "") + ) + }) + } + + const hasSearch = + props.searchParams.query !== "" || + (props.searchParams.automations?.length ?? 0) > 0 || + (props.searchParams.statuses?.length ?? 0) > 0 || + (props.searchParams.stages?.length ?? 0) > 0 || + (props.searchParams.actions?.length ?? 0) > 0 + + return ( +
+ {automationRuns.length === 0 && ( + + + + + + No Automation Runs Found + {hasSearch && ( + + Try adjusting your filters or search query. + + )} + + + + )} + + {automationRuns.map((automationRun) => { + return ( + + ) + })} +
+ ) +} + +const AutomationRunListFooterPagination = async (props: { + basePath: string + searchParams: Record + page: number + communityId: CommunitiesId + children?: React.ReactNode + automationRunsPromise: Promise["execute"]> +}) => { + const search = automationRunSearchParamsCache.all() + + const filterParams = getAutomationRunFilterParamsFromSearch(search) + + const count = await getAutomationRunsCount(props.communityId, { + automations: filterParams.automations, + stages: filterParams.stages, + query: filterParams.query, + }) + + const paginationProps = { + mode: "total" as const, + totalPages: Math.ceil(count / search.perPage), + } + + return ( + + {props.children} + + ) +} + +export const PaginatedAutomationRunList: React.FC = async ( + props +) => { + const search = automationRunSearchParamsCache.parse(props.searchParams) + const filterParams = getAutomationRunFilterParamsFromSearch(search) + + const communitySlug = await getCommunitySlug() + + const basePath = props.basePath ?? `/c/${communitySlug}/activity/automations` + + const automationRunsPromise = getAutomationRuns(props.communityId, { + limit: filterParams.limit, + offset: filterParams.offset, + orderBy: filterParams.orderBy, + orderDirection: filterParams.orderDirection, + automations: filterParams.automations, + stages: filterParams.stages, + query: filterParams.query, + }).execute() + + const [availableAutomations, stages] = await Promise.all([ + autoCache( + db + .selectFrom("automations") + .where("communityId", "=", props.communityId) + .select(["id", "name", "icon"]) + .orderBy("name", "asc") + ).execute(), + getStages({ communityId: props.communityId, userId: null }).execute(), + ]) + + const availableActions = Object.values(Action).map((action) => ({ + id: action, + name: action + .split("_") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "), + })) + + return ( +
+ +
+ + }> + + + +
+ + + +
+
+ ) +} diff --git a/core/app/c/[communitySlug]/activity/automations/AutomationRunListSkeleton.tsx b/core/app/c/[communitySlug]/activity/automations/AutomationRunListSkeleton.tsx new file mode 100644 index 0000000000..82ead286ee --- /dev/null +++ b/core/app/c/[communitySlug]/activity/automations/AutomationRunListSkeleton.tsx @@ -0,0 +1,28 @@ +import { Skeleton } from "ui/skeleton" +import { cn } from "utils" + +export const AutomationRunListSkeleton = ({ + amount = 10, + className, +}: { + amount?: number + className?: string +}) => ( +
+ {Array.from({ length: amount }).map((_, index) => ( + +
+ +
+ + + +
+
+
+ ))} +
+) diff --git a/core/app/c/[communitySlug]/activity/automations/AutomationRunSearchFooter.tsx b/core/app/c/[communitySlug]/activity/automations/AutomationRunSearchFooter.tsx new file mode 100644 index 0000000000..07ac5f902a --- /dev/null +++ b/core/app/c/[communitySlug]/activity/automations/AutomationRunSearchFooter.tsx @@ -0,0 +1,165 @@ +"use client" + +import { Label } from "ui/label" +import { + Pagination, + PaginationContent, + PaginationFirst, + PaginationLast, + PaginationNext, + PaginationPrevious, +} from "ui/pagination" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "ui/select" +import { cn } from "utils" + +import { useAutomationRunSearch } from "./AutomationRunSearchProvider" + +export const AutomationRunSearchFooter = ( + props: { + basePath: string + searchParams: Record + page: number + children?: React.ReactNode + className?: string + } & ( + | { + mode: "total" + totalPages: number + } + | { + mode: "cursor" + hasNextPage: boolean + } + ) +) => { + const { setInputValues } = useAutomationRunSearch() + + const { basePath, searchParams, page, children, className } = props + + const prevDisabled = page <= 1 + const nextDisabled = props.mode === "total" ? page >= props.totalPages : !props.hasNextPage + const showLastButton = props.mode === "total" + + return ( +
+ + + {props.mode === "total" ? ( + + Page {page} of {props.totalPages} + + ) : ( + Page {page} + )} + + + { + setInputValues((old) => ({ ...old, page: 1 })) + }} + /> + { + setInputValues((old) => ({ ...old, page: page - 1 })) + }} + /> + { + setInputValues((old) => ({ ...old, page: page + 1 })) + }} + /> + {showLastButton && ( + { + setInputValues((old) => ({ ...old, page: props.totalPages })) + }} + /> + )} + + + {children} +
+ ) +} + +const PAGE_OPTIONS = [10, 25, 50, 100] + +export const AutomationRunSearchResultsPerPageInput = () => { + const { queryParams, setFilters } = useAutomationRunSearch() + + return ( +
+ + +
+ ) +} diff --git a/core/app/c/[communitySlug]/activity/automations/AutomationRunSearchInput.tsx b/core/app/c/[communitySlug]/activity/automations/AutomationRunSearchInput.tsx new file mode 100644 index 0000000000..c26200e1f3 --- /dev/null +++ b/core/app/c/[communitySlug]/activity/automations/AutomationRunSearchInput.tsx @@ -0,0 +1,151 @@ +"use client" + +import type { AutomationsId, StagesId } from "db/public" + +import { MultiSelect } from "ui/multi-select" +import { Separator } from "ui/separator" +import { cn } from "utils" + +import { FilterPopover } from "~/app/c/[communitySlug]/_components/FilterPopover" +import { SearchBar } from "~/app/c/[communitySlug]/_components/SearchBar" +import { SortDropdown } from "~/app/c/[communitySlug]/_components/SortDropdown" +import { entries } from "~/lib/mapping" +import { useAutomationRunSearch } from "./AutomationRunSearchProvider" + +const sortOptions = [{ id: "createdAt", label: "Created" }] + +const statuses = { + success: "Success", + failure: "Failure", + partial: "Partial", + scheduled: "Scheduled", +} + +export type AutomationRunSearchProps = React.PropsWithChildren + +export const AutomationRunSearch = (props: AutomationRunSearchProps) => { + const { + queryParams, + inputValues, + setQuery, + setFilters, + stale, + availableAutomations, + availableStages, + availableActions, + } = useAutomationRunSearch() + + const activeFilterCount = + queryParams.automations.length + + queryParams.statuses.length + + queryParams.stages.length + + queryParams.actions.length + + return ( +
+ + +
+

Automation

+ ({ + label: automation.name, + value: automation.id, + }))} + defaultValue={queryParams.automations.map( + (automation) => automation.id + )} + onValueChange={(items) => + setFilters((old) => ({ + ...old, + automations: items as AutomationsId[], + })) + } + showClearAll + value={inputValues.automations} + placeholder="Select automations..." + /> +
+ +
+

Status

+ ({ + label, + value: key, + }))} + defaultValue={queryParams.statuses} + onValueChange={(items) => + setFilters((old) => ({ ...old, statuses: items })) + } + value={inputValues.statuses} + showClearAll + placeholder="Select statuses..." + /> +
+ +
+

Stage

+ ({ + label: stage.name, + value: stage.id, + }))} + defaultValue={queryParams.stages.map((stage) => stage.id)} + onValueChange={(items) => + setFilters((old) => ({ + ...old, + stages: items as StagesId[], + })) + } + value={inputValues.stages} + showClearAll + placeholder="Select stages..." + /> +
+ +
+

Action

+ ({ + label: action.name, + value: action.id, + }))} + defaultValue={queryParams.actions.map((action) => action.id)} + onValueChange={(items) => + setFilters((old) => ({ ...old, actions: items })) + } + value={inputValues.actions} + showClearAll + placeholder="Select actions..." + /> +
+
+ + + setFilters((old) => ({ ...old, sort: [{ id, desc }] })) + } + /> + + } + /> +
+ {props.children} +
+
+ ) +} diff --git a/core/app/c/[communitySlug]/activity/automations/AutomationRunSearchProvider.tsx b/core/app/c/[communitySlug]/activity/automations/AutomationRunSearchProvider.tsx new file mode 100644 index 0000000000..3fbf8fb93c --- /dev/null +++ b/core/app/c/[communitySlug]/activity/automations/AutomationRunSearchProvider.tsx @@ -0,0 +1,252 @@ +"use client" + +import type { Automations, Stages } from "db/public" +import type { Dispatch, SetStateAction } from "react" +import type { AutomationRunComputedStatus } from "~/actions/results" +import type { AutomationRunSearchParams } from "./automationRunQuery" + +import { + createContext, + useCallback, + useContext, + useDeferredValue, + useEffect, + useMemo, + useState, +} from "react" +import { useQueryStates } from "nuqs" +import { useDebouncedCallback } from "use-debounce" + +import { automationRunSearchParsers } from "./automationRunQuery" + +type Props = { + children: React.ReactNode + availableAutomations: Automations[] + availableStages: Stages[] + availableActions: { id: string; name: string }[] +} + +type FullAutomationRunSearchParams = Omit< + AutomationRunSearchParams, + "automations" | "statuses" | "stages" | "actions" +> & { + automations: Automations[] + statuses: AutomationRunComputedStatus[] + stages: Stages[] + actions: { id: string; name: string }[] +} + +type AutomationRunSearchContextType = { + queryParams: FullAutomationRunSearchParams + availableAutomations: Automations[] + availableStages: Stages[] + availableActions: { id: string; name: string }[] + inputValues: AutomationRunSearchParams + setQuery: Dispatch> + setFilters: Dispatch> + stale: boolean + setInputValues: Dispatch> +} + +const DEFAULT_SEARCH_PARAMS = { + automations: [], + statuses: [], + stages: [], + actions: [], + filters: [], + query: "", + page: 1, + sort: [{ id: "createdAt", desc: true }], + perPage: 25, +} + +const AutomationRunSearchContext = createContext({ + queryParams: DEFAULT_SEARCH_PARAMS as FullAutomationRunSearchParams, + inputValues: DEFAULT_SEARCH_PARAMS as AutomationRunSearchParams, + availableAutomations: [], + availableStages: [], + availableActions: [], + stale: false, + setQuery: () => "", + setFilters: () => DEFAULT_SEARCH_PARAMS, + setInputValues: () => {}, +}) + +const DEBOUNCE_TIME = 300 + +const isStale = (query: AutomationRunSearchParams, inputValues: AutomationRunSearchParams) => { + if (inputValues.query.length === 1) { + return false + } + + if (query.query !== inputValues.query) { + return true + } + + const sort = query.sort[0] + if (sort.id !== inputValues.sort[0]?.id || sort.desc !== inputValues.sort[0]?.desc) { + return true + } + + if (query.page !== inputValues.page) { + return true + } + + if (query.perPage !== inputValues.perPage) { + return true + } + + if (query.automations) { + const currentAutomationsSet = new Set(query.automations) + const inputAutomationsSet = new Set(inputValues.automations) + + if ( + currentAutomationsSet.difference(inputAutomationsSet).size !== 0 || + inputAutomationsSet.difference(currentAutomationsSet).size !== 0 + ) { + return true + } + } + + if (query.statuses) { + const currentStatusesSet = new Set(query.statuses) + const inputStatusesSet = new Set(inputValues.statuses) + + if ( + currentStatusesSet.difference(inputStatusesSet).size !== 0 || + inputStatusesSet.difference(currentStatusesSet).size !== 0 + ) { + return true + } + } + + if (query.stages) { + const currentStagesSet = new Set(query.stages) + const inputStagesSet = new Set(inputValues.stages) + + if ( + currentStagesSet.difference(inputStagesSet).size !== 0 || + inputStagesSet.difference(currentStagesSet).size !== 0 + ) { + return true + } + } + + if (query.actions) { + const currentActionsSet = new Set(query.actions) + const inputActionsSet = new Set(inputValues.actions) + + if ( + currentActionsSet.difference(inputActionsSet).size !== 0 || + inputActionsSet.difference(currentActionsSet).size !== 0 + ) { + return true + } + } + + return false +} + +export function AutomationRunSearchProvider({ children, ...props }: Props) { + const [queryparams, setQueryParaams] = useQueryStates(automationRunSearchParsers, { + shallow: false, + }) + + const [inputValues, setInputValues] = useState(queryparams) + + const deferredQuery = useDeferredValue(queryparams) + + const currentAutomations = props.availableAutomations?.filter((automation) => + queryparams.automations?.includes(automation.id) + ) + + const currentStatuses = queryparams.statuses as AutomationRunComputedStatus[] + + const currentStages = props.availableStages?.filter((stage) => + queryparams.stages?.includes(stage.id) + ) + + const currentActions = props.availableActions?.filter((action) => + queryparams.actions?.includes(action.id) + ) + + const stale = useMemo(() => isStale(deferredQuery, inputValues), [deferredQuery, inputValues]) + + useEffect(() => { + setInputValues(queryparams) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const debouncedSetQuery = useDebouncedCallback((value: SetStateAction) => { + setQueryParaams((old) => { + const newQuery = typeof value === "function" ? value(old.query) : value + return { ...old, query: newQuery, page: 1 } + }) + }, DEBOUNCE_TIME) + + const setQuery = useCallback( + (value: SetStateAction) => { + setInputValues((old) => { + const newQuery = typeof value === "function" ? value(old.query) : value + return { ...old, query: newQuery, page: 1 } + }) + if (value.length >= 2 || value.length === 0) { + debouncedSetQuery(value) + } + }, + [debouncedSetQuery] + ) + + const setFilters = useCallback( + (filters: SetStateAction) => { + setInputValues((old) => { + const newFilters = typeof filters === "function" ? filters(old) : filters + return { + ...old, + ...newFilters, + } + }) + setQueryParaams((old) => { + const newFilters = typeof filters === "function" ? filters(old) : filters + return { + ...old, + ...newFilters, + } + }) + }, + [setQueryParaams] + ) + + return ( + + {children} + + ) +} + +export const useAutomationRunSearch = () => { + const automationRunSearch = useContext(AutomationRunSearchContext) + return automationRunSearch +} diff --git a/core/app/c/[communitySlug]/activity/automations/automationRunQuery.ts b/core/app/c/[communitySlug]/activity/automations/automationRunQuery.ts new file mode 100644 index 0000000000..e3a940d256 --- /dev/null +++ b/core/app/c/[communitySlug]/activity/automations/automationRunQuery.ts @@ -0,0 +1,65 @@ +import type { Action, ActionRunStatus, AutomationsId, StagesId } from "db/public" +import type { inferParserType } from "nuqs/server" +import type { AutomationRunComputedStatus } from "~/actions/results" + +import { createSearchParamsCache, parseAsArrayOf, parseAsInteger, parseAsString } from "nuqs/server" + +import { getFiltersStateParser, getSortingStateParser } from "ui/data-table-paged" + +const DEFAULT_PAGE_SIZE = 25 + +export type AutomationRunSearchParams = inferParserType + +export const automationRunSearchParsers = { + page: parseAsInteger.withDefault(1), + perPage: parseAsInteger.withDefault(DEFAULT_PAGE_SIZE), + query: parseAsString.withDefault(""), + sort: getSortingStateParser<{ + createdAt: string + }>().withDefault([{ id: "createdAt", desc: true }]), + filters: getFiltersStateParser>().withDefault([]), + automations: parseAsArrayOf(parseAsString).withDefault([]), + statuses: parseAsArrayOf(parseAsString).withDefault([]), + stages: parseAsArrayOf(parseAsString).withDefault([]), + actions: parseAsArrayOf(parseAsString).withDefault([]), +} + +export const automationRunSearchParamsCache = createSearchParamsCache(automationRunSearchParsers) + +export type GetAutomationRunsFilterParams = { + limit: number + offset: number + orderBy: "createdAt" + orderDirection: "desc" | "asc" + automations?: AutomationsId[] + statuses?: (ActionRunStatus | "partial")[] + stages?: StagesId[] + actions?: Action[] + query?: string +} + +export const getAutomationRunFilterParamsFromSearch = ( + search: AutomationRunSearchParams +): GetAutomationRunsFilterParams => { + const sort = search.sort[0] + const limit = search.perPage + const offset = (search.page - 1) * search.perPage + const orderBy = "createdAt" + const orderDirection = sort?.desc ? "desc" : "asc" + + return { + limit, + offset, + orderBy, + orderDirection, + automations: + search.automations.length > 0 ? (search.automations as AutomationsId[]) : undefined, + statuses: + search.statuses.length > 0 + ? (search.statuses as AutomationRunComputedStatus[]) + : undefined, + stages: search.stages.length > 0 ? (search.stages as StagesId[]) : undefined, + actions: search.actions.length > 0 ? (search.actions as Action[]) : undefined, + query: search.query || undefined, + } +} diff --git a/core/app/c/[communitySlug]/activity/actions/page.tsx b/core/app/c/[communitySlug]/activity/automations/page.tsx similarity index 65% rename from core/app/c/[communitySlug]/activity/actions/page.tsx rename to core/app/c/[communitySlug]/activity/automations/page.tsx index 1992628283..026da63809 100644 --- a/core/app/c/[communitySlug]/activity/actions/page.tsx +++ b/core/app/c/[communitySlug]/activity/automations/page.tsx @@ -7,21 +7,21 @@ import { Activity } from "ui/icon" import { getPageLoginData } from "~/lib/authentication/loginData" import { userCan } from "~/lib/authorization/capabilities" -import { getActionRuns } from "~/lib/server/actions" import { findCommunityBySlug } from "~/lib/server/community" import { ContentLayout } from "../../ContentLayout" -import { ActionRunsTable } from "./ActionRunsTable" +import { PaginatedAutomationRunList } from "./AutomationRunList" export const metadata: Metadata = { - title: "Action Log", + title: "Automation Logs", } export default async function Page(props: { params: Promise<{ communitySlug: string }> + searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) { - const params = await props.params + const [params, searchParams] = await Promise.all([props.params, props.searchParams]) const { communitySlug } = params @@ -34,14 +34,11 @@ export default async function Page(props: { notFound() } - const [canEditCommunity, actionRuns] = await Promise.all([ - userCan( - Capabilities.editCommunity, - { type: MembershipType.community, communityId: community.id }, - user.id - ), - getActionRuns(community.id).execute(), - ]) + const canEditCommunity = await userCan( + Capabilities.editCommunity, + { type: MembershipType.community, communityId: community.id }, + user.id + ) if (!canEditCommunity) { redirect(`/c/${communitySlug}/unauthorized`) @@ -51,14 +48,12 @@ export default async function Page(props: { - Action + Automation Logs } > -
- -
+
) } diff --git a/core/app/c/[communitySlug]/developers/docs/stoplight.styles.css b/core/app/c/[communitySlug]/developers/docs/stoplight.styles.css index ac0a1e4077..a3ea8ca33c 100644 --- a/core/app/c/[communitySlug]/developers/docs/stoplight.styles.css +++ b/core/app/c/[communitySlug]/developers/docs/stoplight.styles.css @@ -1 +1,3 @@ +/* biome-ignore-all format: no format please */ +/* biome-ignore-all lint: no lint please */ blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:transparent;background-image:none}:focus{outline:none}fieldset,ol,ul{margin:0;padding:0}ol,ul{list-style:none}html{font-family:var(--font-ui);line-height:1.5}body{text-rendering:optimizeSpeed;font-family:inherit;line-height:inherit;margin:0;min-height:100vh}*,:after,:before{border:0 solid var(--color-border,currentColor);box-sizing:border-box}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-ms-input-placeholder,textarea::-ms-input-placeholder{color:#a1a1aa}input::placeholder,textarea::placeholder{color:#a1a1aa}[role=button],button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{color:inherit;line-height:inherit;padding:0}code,kbd,pre,samp{font-family:var(--font-mono)}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}button{font-family:var(--font-ui)}select{-moz-appearance:none;-webkit-appearance:none}select::-ms-expand{display:none}select{font-size:inherit}iframe{border:0}@media (prefers-reduced-motion:reduce){*{animation-duration:.01ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-duration:.01ms!important}}.sl-stack--1{gap:4px}.sl-stack--2{gap:8px}.sl-stack--3{gap:12px}.sl-stack--4{gap:16px}.sl-stack--5{gap:20px}.sl-stack--6{gap:24px}.sl-stack--7{gap:28px}.sl-stack--8{gap:32px}.sl-stack--9{gap:36px}.sl-stack--10{gap:40px}.sl-stack--12{gap:48px}.sl-stack--14{gap:56px}.sl-stack--16{gap:64px}.sl-stack--20{gap:80px}.sl-stack--24{gap:96px}.sl-stack--32{gap:128px}.sl-content-center{align-content:center}.sl-content-start{align-content:flex-start}.sl-content-end{align-content:flex-end}.sl-content-between{align-content:space-between}.sl-content-around{align-content:space-around}.sl-content-evenly{align-content:space-evenly}.sl-items-start{align-items:flex-start}.sl-items-end{align-items:flex-end}.sl-items-center{align-items:center}.sl-items-baseline{align-items:baseline}.sl-items-stretch{align-items:stretch}.sl-self-auto{align-self:auto}.sl-self-start{align-self:flex-start}.sl-self-end{align-self:flex-end}.sl-self-center{align-self:center}.sl-self-stretch{align-self:stretch}.sl-bg-transparent{background-color:transparent}.sl-bg-current{background-color:currentColor}.sl-bg-lighten-100{background-color:var(--color-lighten-100)}.sl-bg-darken-100{background-color:var(--color-darken-100)}.sl-bg-primary{background-color:var(--color-primary)}.sl-bg-primary-tint{background-color:var(--color-primary-tint)}.sl-bg-primary-light{background-color:var(--color-primary-light)}.sl-bg-primary-dark{background-color:var(--color-primary-dark)}.sl-bg-primary-darker{background-color:var(--color-primary-darker)}.sl-bg-success{background-color:var(--color-success)}.sl-bg-success-tint{background-color:var(--color-success-tint)}.sl-bg-success-light{background-color:var(--color-success-light)}.sl-bg-success-dark{background-color:var(--color-success-dark)}.sl-bg-success-darker{background-color:var(--color-success-darker)}.sl-bg-warning{background-color:var(--color-warning)}.sl-bg-warning-tint{background-color:var(--color-warning-tint)}.sl-bg-warning-light{background-color:var(--color-warning-light)}.sl-bg-warning-dark{background-color:var(--color-warning-dark)}.sl-bg-warning-darker{background-color:var(--color-warning-darker)}.sl-bg-danger{background-color:var(--color-danger)}.sl-bg-danger-tint{background-color:var(--color-danger-tint)}.sl-bg-danger-light{background-color:var(--color-danger-light)}.sl-bg-danger-dark{background-color:var(--color-danger-dark)}.sl-bg-danger-darker{background-color:var(--color-danger-darker)}.sl-bg-code{background-color:var(--color-code)}.sl-bg-on-code{background-color:var(--color-on-code)}.sl-bg-on-primary{background-color:var(--color-on-primary)}.sl-bg-on-success{background-color:var(--color-on-success)}.sl-bg-on-warning{background-color:var(--color-on-warning)}.sl-bg-on-danger{background-color:var(--color-on-danger)}.sl-bg-canvas-50{background-color:var(--color-canvas-50)}.sl-bg-canvas-100{background-color:var(--color-canvas-100)}.sl-bg-canvas-200{background-color:var(--color-canvas-200)}.sl-bg-canvas-300{background-color:var(--color-canvas-300)}.sl-bg-canvas-400{background-color:var(--color-canvas-400)}.sl-bg-canvas-500{background-color:var(--color-canvas-500)}.sl-bg-canvas-dark{background-color:var(--color-canvas-dark)}.sl-bg-canvas-pure{background-color:var(--color-canvas-pure)}.sl-bg-canvas{background-color:var(--color-canvas)}.sl-bg-canvas-tint{background-color:var(--color-canvas-tint)}.sl-bg-canvas-dialog{background-color:var(--color-canvas-dialog)}.sl-bg-body{background-color:var(--color-text)}.sl-bg-body-muted{background-color:var(--color-text-muted)}.sl-bg-body-light{background-color:var(--color-text-light)}.hover\:sl-bg-transparent:hover{background-color:transparent}.hover\:sl-bg-current:hover{background-color:currentColor}.hover\:sl-bg-lighten-100:hover{background-color:var(--color-lighten-100)}.hover\:sl-bg-darken-100:hover{background-color:var(--color-darken-100)}.hover\:sl-bg-primary:hover{background-color:var(--color-primary)}.hover\:sl-bg-primary-tint:hover{background-color:var(--color-primary-tint)}.hover\:sl-bg-primary-light:hover{background-color:var(--color-primary-light)}.hover\:sl-bg-primary-dark:hover{background-color:var(--color-primary-dark)}.hover\:sl-bg-primary-darker:hover{background-color:var(--color-primary-darker)}.hover\:sl-bg-success:hover{background-color:var(--color-success)}.hover\:sl-bg-success-tint:hover{background-color:var(--color-success-tint)}.hover\:sl-bg-success-light:hover{background-color:var(--color-success-light)}.hover\:sl-bg-success-dark:hover{background-color:var(--color-success-dark)}.hover\:sl-bg-success-darker:hover{background-color:var(--color-success-darker)}.hover\:sl-bg-warning:hover{background-color:var(--color-warning)}.hover\:sl-bg-warning-tint:hover{background-color:var(--color-warning-tint)}.hover\:sl-bg-warning-light:hover{background-color:var(--color-warning-light)}.hover\:sl-bg-warning-dark:hover{background-color:var(--color-warning-dark)}.hover\:sl-bg-warning-darker:hover{background-color:var(--color-warning-darker)}.hover\:sl-bg-danger:hover{background-color:var(--color-danger)}.hover\:sl-bg-danger-tint:hover{background-color:var(--color-danger-tint)}.hover\:sl-bg-danger-light:hover{background-color:var(--color-danger-light)}.hover\:sl-bg-danger-dark:hover{background-color:var(--color-danger-dark)}.hover\:sl-bg-danger-darker:hover{background-color:var(--color-danger-darker)}.hover\:sl-bg-code:hover{background-color:var(--color-code)}.hover\:sl-bg-on-code:hover{background-color:var(--color-on-code)}.hover\:sl-bg-on-primary:hover{background-color:var(--color-on-primary)}.hover\:sl-bg-on-success:hover{background-color:var(--color-on-success)}.hover\:sl-bg-on-warning:hover{background-color:var(--color-on-warning)}.hover\:sl-bg-on-danger:hover{background-color:var(--color-on-danger)}.hover\:sl-bg-canvas-50:hover{background-color:var(--color-canvas-50)}.hover\:sl-bg-canvas-100:hover{background-color:var(--color-canvas-100)}.hover\:sl-bg-canvas-200:hover{background-color:var(--color-canvas-200)}.hover\:sl-bg-canvas-300:hover{background-color:var(--color-canvas-300)}.hover\:sl-bg-canvas-400:hover{background-color:var(--color-canvas-400)}.hover\:sl-bg-canvas-500:hover{background-color:var(--color-canvas-500)}.hover\:sl-bg-canvas-dark:hover{background-color:var(--color-canvas-dark)}.hover\:sl-bg-canvas-pure:hover{background-color:var(--color-canvas-pure)}.hover\:sl-bg-canvas:hover{background-color:var(--color-canvas)}.hover\:sl-bg-canvas-tint:hover{background-color:var(--color-canvas-tint)}.hover\:sl-bg-canvas-dialog:hover{background-color:var(--color-canvas-dialog)}.hover\:sl-bg-body:hover{background-color:var(--color-text)}.hover\:sl-bg-body-muted:hover{background-color:var(--color-text-muted)}.hover\:sl-bg-body-light:hover{background-color:var(--color-text-light)}.focus\:sl-bg-transparent:focus{background-color:transparent}.focus\:sl-bg-current:focus{background-color:currentColor}.focus\:sl-bg-lighten-100:focus{background-color:var(--color-lighten-100)}.focus\:sl-bg-darken-100:focus{background-color:var(--color-darken-100)}.focus\:sl-bg-primary:focus{background-color:var(--color-primary)}.focus\:sl-bg-primary-tint:focus{background-color:var(--color-primary-tint)}.focus\:sl-bg-primary-light:focus{background-color:var(--color-primary-light)}.focus\:sl-bg-primary-dark:focus{background-color:var(--color-primary-dark)}.focus\:sl-bg-primary-darker:focus{background-color:var(--color-primary-darker)}.focus\:sl-bg-success:focus{background-color:var(--color-success)}.focus\:sl-bg-success-tint:focus{background-color:var(--color-success-tint)}.focus\:sl-bg-success-light:focus{background-color:var(--color-success-light)}.focus\:sl-bg-success-dark:focus{background-color:var(--color-success-dark)}.focus\:sl-bg-success-darker:focus{background-color:var(--color-success-darker)}.focus\:sl-bg-warning:focus{background-color:var(--color-warning)}.focus\:sl-bg-warning-tint:focus{background-color:var(--color-warning-tint)}.focus\:sl-bg-warning-light:focus{background-color:var(--color-warning-light)}.focus\:sl-bg-warning-dark:focus{background-color:var(--color-warning-dark)}.focus\:sl-bg-warning-darker:focus{background-color:var(--color-warning-darker)}.focus\:sl-bg-danger:focus{background-color:var(--color-danger)}.focus\:sl-bg-danger-tint:focus{background-color:var(--color-danger-tint)}.focus\:sl-bg-danger-light:focus{background-color:var(--color-danger-light)}.focus\:sl-bg-danger-dark:focus{background-color:var(--color-danger-dark)}.focus\:sl-bg-danger-darker:focus{background-color:var(--color-danger-darker)}.focus\:sl-bg-code:focus{background-color:var(--color-code)}.focus\:sl-bg-on-code:focus{background-color:var(--color-on-code)}.focus\:sl-bg-on-primary:focus{background-color:var(--color-on-primary)}.focus\:sl-bg-on-success:focus{background-color:var(--color-on-success)}.focus\:sl-bg-on-warning:focus{background-color:var(--color-on-warning)}.focus\:sl-bg-on-danger:focus{background-color:var(--color-on-danger)}.focus\:sl-bg-canvas-50:focus{background-color:var(--color-canvas-50)}.focus\:sl-bg-canvas-100:focus{background-color:var(--color-canvas-100)}.focus\:sl-bg-canvas-200:focus{background-color:var(--color-canvas-200)}.focus\:sl-bg-canvas-300:focus{background-color:var(--color-canvas-300)}.focus\:sl-bg-canvas-400:focus{background-color:var(--color-canvas-400)}.focus\:sl-bg-canvas-500:focus{background-color:var(--color-canvas-500)}.focus\:sl-bg-canvas-dark:focus{background-color:var(--color-canvas-dark)}.focus\:sl-bg-canvas-pure:focus{background-color:var(--color-canvas-pure)}.focus\:sl-bg-canvas:focus{background-color:var(--color-canvas)}.focus\:sl-bg-canvas-tint:focus{background-color:var(--color-canvas-tint)}.focus\:sl-bg-canvas-dialog:focus{background-color:var(--color-canvas-dialog)}.focus\:sl-bg-body:focus{background-color:var(--color-text)}.focus\:sl-bg-body-muted:focus{background-color:var(--color-text-muted)}.focus\:sl-bg-body-light:focus{background-color:var(--color-text-light)}.active\:sl-bg-transparent:active{background-color:transparent}.active\:sl-bg-current:active{background-color:currentColor}.active\:sl-bg-lighten-100:active{background-color:var(--color-lighten-100)}.active\:sl-bg-darken-100:active{background-color:var(--color-darken-100)}.active\:sl-bg-primary:active{background-color:var(--color-primary)}.active\:sl-bg-primary-tint:active{background-color:var(--color-primary-tint)}.active\:sl-bg-primary-light:active{background-color:var(--color-primary-light)}.active\:sl-bg-primary-dark:active{background-color:var(--color-primary-dark)}.active\:sl-bg-primary-darker:active{background-color:var(--color-primary-darker)}.active\:sl-bg-success:active{background-color:var(--color-success)}.active\:sl-bg-success-tint:active{background-color:var(--color-success-tint)}.active\:sl-bg-success-light:active{background-color:var(--color-success-light)}.active\:sl-bg-success-dark:active{background-color:var(--color-success-dark)}.active\:sl-bg-success-darker:active{background-color:var(--color-success-darker)}.active\:sl-bg-warning:active{background-color:var(--color-warning)}.active\:sl-bg-warning-tint:active{background-color:var(--color-warning-tint)}.active\:sl-bg-warning-light:active{background-color:var(--color-warning-light)}.active\:sl-bg-warning-dark:active{background-color:var(--color-warning-dark)}.active\:sl-bg-warning-darker:active{background-color:var(--color-warning-darker)}.active\:sl-bg-danger:active{background-color:var(--color-danger)}.active\:sl-bg-danger-tint:active{background-color:var(--color-danger-tint)}.active\:sl-bg-danger-light:active{background-color:var(--color-danger-light)}.active\:sl-bg-danger-dark:active{background-color:var(--color-danger-dark)}.active\:sl-bg-danger-darker:active{background-color:var(--color-danger-darker)}.active\:sl-bg-code:active{background-color:var(--color-code)}.active\:sl-bg-on-code:active{background-color:var(--color-on-code)}.active\:sl-bg-on-primary:active{background-color:var(--color-on-primary)}.active\:sl-bg-on-success:active{background-color:var(--color-on-success)}.active\:sl-bg-on-warning:active{background-color:var(--color-on-warning)}.active\:sl-bg-on-danger:active{background-color:var(--color-on-danger)}.active\:sl-bg-canvas-50:active{background-color:var(--color-canvas-50)}.active\:sl-bg-canvas-100:active{background-color:var(--color-canvas-100)}.active\:sl-bg-canvas-200:active{background-color:var(--color-canvas-200)}.active\:sl-bg-canvas-300:active{background-color:var(--color-canvas-300)}.active\:sl-bg-canvas-400:active{background-color:var(--color-canvas-400)}.active\:sl-bg-canvas-500:active{background-color:var(--color-canvas-500)}.active\:sl-bg-canvas-dark:active{background-color:var(--color-canvas-dark)}.active\:sl-bg-canvas-pure:active{background-color:var(--color-canvas-pure)}.active\:sl-bg-canvas:active{background-color:var(--color-canvas)}.active\:sl-bg-canvas-tint:active{background-color:var(--color-canvas-tint)}.active\:sl-bg-canvas-dialog:active{background-color:var(--color-canvas-dialog)}.active\:sl-bg-body:active{background-color:var(--color-text)}.active\:sl-bg-body-muted:active{background-color:var(--color-text-muted)}.active\:sl-bg-body-light:active{background-color:var(--color-text-light)}.disabled\:sl-bg-transparent:disabled{background-color:transparent}.disabled\:sl-bg-current:disabled{background-color:currentColor}.disabled\:sl-bg-lighten-100:disabled{background-color:var(--color-lighten-100)}.disabled\:sl-bg-darken-100:disabled{background-color:var(--color-darken-100)}.disabled\:sl-bg-primary:disabled{background-color:var(--color-primary)}.disabled\:sl-bg-primary-tint:disabled{background-color:var(--color-primary-tint)}.disabled\:sl-bg-primary-light:disabled{background-color:var(--color-primary-light)}.disabled\:sl-bg-primary-dark:disabled{background-color:var(--color-primary-dark)}.disabled\:sl-bg-primary-darker:disabled{background-color:var(--color-primary-darker)}.disabled\:sl-bg-success:disabled{background-color:var(--color-success)}.disabled\:sl-bg-success-tint:disabled{background-color:var(--color-success-tint)}.disabled\:sl-bg-success-light:disabled{background-color:var(--color-success-light)}.disabled\:sl-bg-success-dark:disabled{background-color:var(--color-success-dark)}.disabled\:sl-bg-success-darker:disabled{background-color:var(--color-success-darker)}.disabled\:sl-bg-warning:disabled{background-color:var(--color-warning)}.disabled\:sl-bg-warning-tint:disabled{background-color:var(--color-warning-tint)}.disabled\:sl-bg-warning-light:disabled{background-color:var(--color-warning-light)}.disabled\:sl-bg-warning-dark:disabled{background-color:var(--color-warning-dark)}.disabled\:sl-bg-warning-darker:disabled{background-color:var(--color-warning-darker)}.disabled\:sl-bg-danger:disabled{background-color:var(--color-danger)}.disabled\:sl-bg-danger-tint:disabled{background-color:var(--color-danger-tint)}.disabled\:sl-bg-danger-light:disabled{background-color:var(--color-danger-light)}.disabled\:sl-bg-danger-dark:disabled{background-color:var(--color-danger-dark)}.disabled\:sl-bg-danger-darker:disabled{background-color:var(--color-danger-darker)}.disabled\:sl-bg-code:disabled{background-color:var(--color-code)}.disabled\:sl-bg-on-code:disabled{background-color:var(--color-on-code)}.disabled\:sl-bg-on-primary:disabled{background-color:var(--color-on-primary)}.disabled\:sl-bg-on-success:disabled{background-color:var(--color-on-success)}.disabled\:sl-bg-on-warning:disabled{background-color:var(--color-on-warning)}.disabled\:sl-bg-on-danger:disabled{background-color:var(--color-on-danger)}.disabled\:sl-bg-canvas-50:disabled{background-color:var(--color-canvas-50)}.disabled\:sl-bg-canvas-100:disabled{background-color:var(--color-canvas-100)}.disabled\:sl-bg-canvas-200:disabled{background-color:var(--color-canvas-200)}.disabled\:sl-bg-canvas-300:disabled{background-color:var(--color-canvas-300)}.disabled\:sl-bg-canvas-400:disabled{background-color:var(--color-canvas-400)}.disabled\:sl-bg-canvas-500:disabled{background-color:var(--color-canvas-500)}.disabled\:sl-bg-canvas-dark:disabled{background-color:var(--color-canvas-dark)}.disabled\:sl-bg-canvas-pure:disabled{background-color:var(--color-canvas-pure)}.disabled\:sl-bg-canvas:disabled{background-color:var(--color-canvas)}.disabled\:sl-bg-canvas-tint:disabled{background-color:var(--color-canvas-tint)}.disabled\:sl-bg-canvas-dialog:disabled{background-color:var(--color-canvas-dialog)}.disabled\:sl-bg-body:disabled{background-color:var(--color-text)}.disabled\:sl-bg-body-muted:disabled{background-color:var(--color-text-muted)}.disabled\:sl-bg-body-light:disabled{background-color:var(--color-text-light)}.sl-bg-none{background-image:none}.sl-bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.sl-bg-gradient-to-tr{background-image:linear-gradient(to top right,var(--tw-gradient-stops))}.sl-bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.sl-bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.sl-bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.sl-bg-gradient-to-bl{background-image:linear-gradient(to bottom left,var(--tw-gradient-stops))}.sl-bg-gradient-to-l{background-image:linear-gradient(to left,var(--tw-gradient-stops))}.sl-bg-gradient-to-tl{background-image:linear-gradient(to top left,var(--tw-gradient-stops))}.sl-blur-0,.sl-blur-none{--tw-blur:blur(0)}.sl-blur-sm{--tw-blur:blur(4px)}.sl-blur{--tw-blur:blur(8px)}.sl-blur-md{--tw-blur:blur(12px)}.sl-blur-lg{--tw-blur:blur(16px)}.sl-blur-xl{--tw-blur:blur(24px)}.sl-blur-2xl{--tw-blur:blur(40px)}.sl-blur-3xl{--tw-blur:blur(64px)}.sl-border-transparent{border-color:transparent}.sl-border-current{border-color:currentColor}.sl-border-lighten-100{border-color:var(--color-lighten-100)}.sl-border-darken-100{border-color:var(--color-darken-100)}.sl-border-primary{border-color:var(--color-primary)}.sl-border-primary-tint{border-color:var(--color-primary-tint)}.sl-border-primary-light{border-color:var(--color-primary-light)}.sl-border-primary-dark{border-color:var(--color-primary-dark)}.sl-border-primary-darker{border-color:var(--color-primary-darker)}.sl-border-success{border-color:var(--color-success)}.sl-border-success-tint{border-color:var(--color-success-tint)}.sl-border-success-light{border-color:var(--color-success-light)}.sl-border-success-dark{border-color:var(--color-success-dark)}.sl-border-success-darker{border-color:var(--color-success-darker)}.sl-border-warning{border-color:var(--color-warning)}.sl-border-warning-tint{border-color:var(--color-warning-tint)}.sl-border-warning-light{border-color:var(--color-warning-light)}.sl-border-warning-dark{border-color:var(--color-warning-dark)}.sl-border-warning-darker{border-color:var(--color-warning-darker)}.sl-border-danger{border-color:var(--color-danger)}.sl-border-danger-tint{border-color:var(--color-danger-tint)}.sl-border-danger-light{border-color:var(--color-danger-light)}.sl-border-danger-dark{border-color:var(--color-danger-dark)}.sl-border-danger-darker{border-color:var(--color-danger-darker)}.sl-border-code{border-color:var(--color-code)}.sl-border-on-code{border-color:var(--color-on-code)}.sl-border-on-primary{border-color:var(--color-on-primary)}.sl-border-on-success{border-color:var(--color-on-success)}.sl-border-on-warning{border-color:var(--color-on-warning)}.sl-border-on-danger{border-color:var(--color-on-danger)}.sl-border-light{border-color:var(--color-border-light)}.sl-border-dark{border-color:var(--color-border-dark)}.sl-border-button{border-color:var(--color-border-button)}.sl-border-input{border-color:var(--color-border-input)}.sl-border-body{border-color:var(--color-text)}.hover\:sl-border-transparent:hover{border-color:transparent}.hover\:sl-border-current:hover{border-color:currentColor}.hover\:sl-border-lighten-100:hover{border-color:var(--color-lighten-100)}.hover\:sl-border-darken-100:hover{border-color:var(--color-darken-100)}.hover\:sl-border-primary:hover{border-color:var(--color-primary)}.hover\:sl-border-primary-tint:hover{border-color:var(--color-primary-tint)}.hover\:sl-border-primary-light:hover{border-color:var(--color-primary-light)}.hover\:sl-border-primary-dark:hover{border-color:var(--color-primary-dark)}.hover\:sl-border-primary-darker:hover{border-color:var(--color-primary-darker)}.hover\:sl-border-success:hover{border-color:var(--color-success)}.hover\:sl-border-success-tint:hover{border-color:var(--color-success-tint)}.hover\:sl-border-success-light:hover{border-color:var(--color-success-light)}.hover\:sl-border-success-dark:hover{border-color:var(--color-success-dark)}.hover\:sl-border-success-darker:hover{border-color:var(--color-success-darker)}.hover\:sl-border-warning:hover{border-color:var(--color-warning)}.hover\:sl-border-warning-tint:hover{border-color:var(--color-warning-tint)}.hover\:sl-border-warning-light:hover{border-color:var(--color-warning-light)}.hover\:sl-border-warning-dark:hover{border-color:var(--color-warning-dark)}.hover\:sl-border-warning-darker:hover{border-color:var(--color-warning-darker)}.hover\:sl-border-danger:hover{border-color:var(--color-danger)}.hover\:sl-border-danger-tint:hover{border-color:var(--color-danger-tint)}.hover\:sl-border-danger-light:hover{border-color:var(--color-danger-light)}.hover\:sl-border-danger-dark:hover{border-color:var(--color-danger-dark)}.hover\:sl-border-danger-darker:hover{border-color:var(--color-danger-darker)}.hover\:sl-border-code:hover{border-color:var(--color-code)}.hover\:sl-border-on-code:hover{border-color:var(--color-on-code)}.hover\:sl-border-on-primary:hover{border-color:var(--color-on-primary)}.hover\:sl-border-on-success:hover{border-color:var(--color-on-success)}.hover\:sl-border-on-warning:hover{border-color:var(--color-on-warning)}.hover\:sl-border-on-danger:hover{border-color:var(--color-on-danger)}.hover\:sl-border-light:hover{border-color:var(--color-border-light)}.hover\:sl-border-dark:hover{border-color:var(--color-border-dark)}.hover\:sl-border-button:hover{border-color:var(--color-border-button)}.hover\:sl-border-input:hover{border-color:var(--color-border-input)}.hover\:sl-border-body:hover{border-color:var(--color-text)}.focus\:sl-border-transparent:focus{border-color:transparent}.focus\:sl-border-current:focus{border-color:currentColor}.focus\:sl-border-lighten-100:focus{border-color:var(--color-lighten-100)}.focus\:sl-border-darken-100:focus{border-color:var(--color-darken-100)}.focus\:sl-border-primary:focus{border-color:var(--color-primary)}.focus\:sl-border-primary-tint:focus{border-color:var(--color-primary-tint)}.focus\:sl-border-primary-light:focus{border-color:var(--color-primary-light)}.focus\:sl-border-primary-dark:focus{border-color:var(--color-primary-dark)}.focus\:sl-border-primary-darker:focus{border-color:var(--color-primary-darker)}.focus\:sl-border-success:focus{border-color:var(--color-success)}.focus\:sl-border-success-tint:focus{border-color:var(--color-success-tint)}.focus\:sl-border-success-light:focus{border-color:var(--color-success-light)}.focus\:sl-border-success-dark:focus{border-color:var(--color-success-dark)}.focus\:sl-border-success-darker:focus{border-color:var(--color-success-darker)}.focus\:sl-border-warning:focus{border-color:var(--color-warning)}.focus\:sl-border-warning-tint:focus{border-color:var(--color-warning-tint)}.focus\:sl-border-warning-light:focus{border-color:var(--color-warning-light)}.focus\:sl-border-warning-dark:focus{border-color:var(--color-warning-dark)}.focus\:sl-border-warning-darker:focus{border-color:var(--color-warning-darker)}.focus\:sl-border-danger:focus{border-color:var(--color-danger)}.focus\:sl-border-danger-tint:focus{border-color:var(--color-danger-tint)}.focus\:sl-border-danger-light:focus{border-color:var(--color-danger-light)}.focus\:sl-border-danger-dark:focus{border-color:var(--color-danger-dark)}.focus\:sl-border-danger-darker:focus{border-color:var(--color-danger-darker)}.focus\:sl-border-code:focus{border-color:var(--color-code)}.focus\:sl-border-on-code:focus{border-color:var(--color-on-code)}.focus\:sl-border-on-primary:focus{border-color:var(--color-on-primary)}.focus\:sl-border-on-success:focus{border-color:var(--color-on-success)}.focus\:sl-border-on-warning:focus{border-color:var(--color-on-warning)}.focus\:sl-border-on-danger:focus{border-color:var(--color-on-danger)}.focus\:sl-border-light:focus{border-color:var(--color-border-light)}.focus\:sl-border-dark:focus{border-color:var(--color-border-dark)}.focus\:sl-border-button:focus{border-color:var(--color-border-button)}.focus\:sl-border-input:focus{border-color:var(--color-border-input)}.focus\:sl-border-body:focus{border-color:var(--color-text)}.focus-within\:sl-border-transparent:focus-within{border-color:transparent}.focus-within\:sl-border-current:focus-within{border-color:currentColor}.focus-within\:sl-border-lighten-100:focus-within{border-color:var(--color-lighten-100)}.focus-within\:sl-border-darken-100:focus-within{border-color:var(--color-darken-100)}.focus-within\:sl-border-primary:focus-within{border-color:var(--color-primary)}.focus-within\:sl-border-primary-tint:focus-within{border-color:var(--color-primary-tint)}.focus-within\:sl-border-primary-light:focus-within{border-color:var(--color-primary-light)}.focus-within\:sl-border-primary-dark:focus-within{border-color:var(--color-primary-dark)}.focus-within\:sl-border-primary-darker:focus-within{border-color:var(--color-primary-darker)}.focus-within\:sl-border-success:focus-within{border-color:var(--color-success)}.focus-within\:sl-border-success-tint:focus-within{border-color:var(--color-success-tint)}.focus-within\:sl-border-success-light:focus-within{border-color:var(--color-success-light)}.focus-within\:sl-border-success-dark:focus-within{border-color:var(--color-success-dark)}.focus-within\:sl-border-success-darker:focus-within{border-color:var(--color-success-darker)}.focus-within\:sl-border-warning:focus-within{border-color:var(--color-warning)}.focus-within\:sl-border-warning-tint:focus-within{border-color:var(--color-warning-tint)}.focus-within\:sl-border-warning-light:focus-within{border-color:var(--color-warning-light)}.focus-within\:sl-border-warning-dark:focus-within{border-color:var(--color-warning-dark)}.focus-within\:sl-border-warning-darker:focus-within{border-color:var(--color-warning-darker)}.focus-within\:sl-border-danger:focus-within{border-color:var(--color-danger)}.focus-within\:sl-border-danger-tint:focus-within{border-color:var(--color-danger-tint)}.focus-within\:sl-border-danger-light:focus-within{border-color:var(--color-danger-light)}.focus-within\:sl-border-danger-dark:focus-within{border-color:var(--color-danger-dark)}.focus-within\:sl-border-danger-darker:focus-within{border-color:var(--color-danger-darker)}.focus-within\:sl-border-code:focus-within{border-color:var(--color-code)}.focus-within\:sl-border-on-code:focus-within{border-color:var(--color-on-code)}.focus-within\:sl-border-on-primary:focus-within{border-color:var(--color-on-primary)}.focus-within\:sl-border-on-success:focus-within{border-color:var(--color-on-success)}.focus-within\:sl-border-on-warning:focus-within{border-color:var(--color-on-warning)}.focus-within\:sl-border-on-danger:focus-within{border-color:var(--color-on-danger)}.focus-within\:sl-border-light:focus-within{border-color:var(--color-border-light)}.focus-within\:sl-border-dark:focus-within{border-color:var(--color-border-dark)}.focus-within\:sl-border-button:focus-within{border-color:var(--color-border-button)}.focus-within\:sl-border-input:focus-within{border-color:var(--color-border-input)}.focus-within\:sl-border-body:focus-within{border-color:var(--color-text)}.active\:sl-border-transparent:active{border-color:transparent}.active\:sl-border-current:active{border-color:currentColor}.active\:sl-border-lighten-100:active{border-color:var(--color-lighten-100)}.active\:sl-border-darken-100:active{border-color:var(--color-darken-100)}.active\:sl-border-primary:active{border-color:var(--color-primary)}.active\:sl-border-primary-tint:active{border-color:var(--color-primary-tint)}.active\:sl-border-primary-light:active{border-color:var(--color-primary-light)}.active\:sl-border-primary-dark:active{border-color:var(--color-primary-dark)}.active\:sl-border-primary-darker:active{border-color:var(--color-primary-darker)}.active\:sl-border-success:active{border-color:var(--color-success)}.active\:sl-border-success-tint:active{border-color:var(--color-success-tint)}.active\:sl-border-success-light:active{border-color:var(--color-success-light)}.active\:sl-border-success-dark:active{border-color:var(--color-success-dark)}.active\:sl-border-success-darker:active{border-color:var(--color-success-darker)}.active\:sl-border-warning:active{border-color:var(--color-warning)}.active\:sl-border-warning-tint:active{border-color:var(--color-warning-tint)}.active\:sl-border-warning-light:active{border-color:var(--color-warning-light)}.active\:sl-border-warning-dark:active{border-color:var(--color-warning-dark)}.active\:sl-border-warning-darker:active{border-color:var(--color-warning-darker)}.active\:sl-border-danger:active{border-color:var(--color-danger)}.active\:sl-border-danger-tint:active{border-color:var(--color-danger-tint)}.active\:sl-border-danger-light:active{border-color:var(--color-danger-light)}.active\:sl-border-danger-dark:active{border-color:var(--color-danger-dark)}.active\:sl-border-danger-darker:active{border-color:var(--color-danger-darker)}.active\:sl-border-code:active{border-color:var(--color-code)}.active\:sl-border-on-code:active{border-color:var(--color-on-code)}.active\:sl-border-on-primary:active{border-color:var(--color-on-primary)}.active\:sl-border-on-success:active{border-color:var(--color-on-success)}.active\:sl-border-on-warning:active{border-color:var(--color-on-warning)}.active\:sl-border-on-danger:active{border-color:var(--color-on-danger)}.active\:sl-border-light:active{border-color:var(--color-border-light)}.active\:sl-border-dark:active{border-color:var(--color-border-dark)}.active\:sl-border-button:active{border-color:var(--color-border-button)}.active\:sl-border-input:active{border-color:var(--color-border-input)}.active\:sl-border-body:active{border-color:var(--color-text)}.sl-rounded-none{border-radius:0}.sl-rounded-sm{border-radius:1px}.sl-rounded{border-radius:2px}.sl-rounded-lg{border-radius:5px}.sl-rounded-xl{border-radius:7px}.sl-rounded-full{border-radius:9999px}.sl-rounded-t-none{border-top-left-radius:0;border-top-right-radius:0}.sl-rounded-r-none{border-bottom-right-radius:0;border-top-right-radius:0}.sl-rounded-b-none{border-bottom-left-radius:0;border-bottom-right-radius:0}.sl-rounded-l-none{border-bottom-left-radius:0;border-top-left-radius:0}.sl-rounded-t-sm{border-top-left-radius:1px;border-top-right-radius:1px}.sl-rounded-r-sm{border-bottom-right-radius:1px;border-top-right-radius:1px}.sl-rounded-b-sm{border-bottom-left-radius:1px;border-bottom-right-radius:1px}.sl-rounded-l-sm{border-bottom-left-radius:1px;border-top-left-radius:1px}.sl-rounded-t{border-top-left-radius:2px}.sl-rounded-r,.sl-rounded-t{border-top-right-radius:2px}.sl-rounded-b,.sl-rounded-r{border-bottom-right-radius:2px}.sl-rounded-b,.sl-rounded-l{border-bottom-left-radius:2px}.sl-rounded-l{border-top-left-radius:2px}.sl-rounded-t-lg{border-top-left-radius:5px;border-top-right-radius:5px}.sl-rounded-r-lg{border-bottom-right-radius:5px;border-top-right-radius:5px}.sl-rounded-b-lg{border-bottom-left-radius:5px;border-bottom-right-radius:5px}.sl-rounded-l-lg{border-bottom-left-radius:5px;border-top-left-radius:5px}.sl-rounded-t-xl{border-top-left-radius:7px;border-top-right-radius:7px}.sl-rounded-r-xl{border-bottom-right-radius:7px;border-top-right-radius:7px}.sl-rounded-b-xl{border-bottom-left-radius:7px;border-bottom-right-radius:7px}.sl-rounded-l-xl{border-bottom-left-radius:7px;border-top-left-radius:7px}.sl-rounded-t-full{border-top-left-radius:9999px;border-top-right-radius:9999px}.sl-rounded-r-full{border-bottom-right-radius:9999px;border-top-right-radius:9999px}.sl-rounded-b-full{border-bottom-left-radius:9999px;border-bottom-right-radius:9999px}.sl-rounded-l-full{border-bottom-left-radius:9999px;border-top-left-radius:9999px}.sl-rounded-tl-none{border-top-left-radius:0}.sl-rounded-tr-none{border-top-right-radius:0}.sl-rounded-br-none{border-bottom-right-radius:0}.sl-rounded-bl-none{border-bottom-left-radius:0}.sl-rounded-tl-sm{border-top-left-radius:1px}.sl-rounded-tr-sm{border-top-right-radius:1px}.sl-rounded-br-sm{border-bottom-right-radius:1px}.sl-rounded-bl-sm{border-bottom-left-radius:1px}.sl-rounded-tl{border-top-left-radius:2px}.sl-rounded-tr{border-top-right-radius:2px}.sl-rounded-br{border-bottom-right-radius:2px}.sl-rounded-bl{border-bottom-left-radius:2px}.sl-rounded-tl-lg{border-top-left-radius:5px}.sl-rounded-tr-lg{border-top-right-radius:5px}.sl-rounded-br-lg{border-bottom-right-radius:5px}.sl-rounded-bl-lg{border-bottom-left-radius:5px}.sl-rounded-tl-xl{border-top-left-radius:7px}.sl-rounded-tr-xl{border-top-right-radius:7px}.sl-rounded-br-xl{border-bottom-right-radius:7px}.sl-rounded-bl-xl{border-bottom-left-radius:7px}.sl-rounded-tl-full{border-top-left-radius:9999px}.sl-rounded-tr-full{border-top-right-radius:9999px}.sl-rounded-br-full{border-bottom-right-radius:9999px}.sl-rounded-bl-full{border-bottom-left-radius:9999px}.sl-border-solid{border-style:solid}.sl-border-dashed{border-style:dashed}.sl-border-dotted{border-style:dotted}.sl-border-double{border-style:double}.sl-border-none{border-style:none}.sl-border-0{border-width:0}.sl-border-2{border-width:2px}.sl-border-4{border-width:4px}.sl-border-8{border-width:8px}.sl-border{border-width:1px}.sl-border-t-0{border-top-width:0}.sl-border-r-0{border-right-width:0}.sl-border-b-0{border-bottom-width:0}.sl-border-l-0{border-left-width:0}.sl-border-t-2{border-top-width:2px}.sl-border-r-2{border-right-width:2px}.sl-border-b-2{border-bottom-width:2px}.sl-border-l-2{border-left-width:2px}.sl-border-t-4{border-top-width:4px}.sl-border-r-4{border-right-width:4px}.sl-border-b-4{border-bottom-width:4px}.sl-border-l-4{border-left-width:4px}.sl-border-t-8{border-top-width:8px}.sl-border-r-8{border-right-width:8px}.sl-border-b-8{border-bottom-width:8px}.sl-border-l-8{border-left-width:8px}.sl-border-t{border-top-width:1px}.sl-border-r{border-right-width:1px}.sl-border-b{border-bottom-width:1px}.sl-border-l{border-left-width:1px}*{--tw-shadow:0 0 #0000}.sl-shadow-sm{--tw-shadow:var(--shadow-sm);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.sl-shadow,.sl-shadow-md{--tw-shadow:var(--shadow-md)}.sl-shadow,.sl-shadow-lg,.sl-shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.sl-shadow-lg{--tw-shadow:var(--shadow-lg)}.sl-shadow-xl{--tw-shadow:var(--shadow-xl)}.sl-shadow-2xl,.sl-shadow-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.sl-shadow-2xl{--tw-shadow:var(--shadow-2xl)}.hover\:sl-shadow-sm:hover{--tw-shadow:var(--shadow-sm);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\:sl-shadow:hover{--tw-shadow:var(--shadow-md);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\:sl-shadow-md:hover{--tw-shadow:var(--shadow-md);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\:sl-shadow-lg:hover{--tw-shadow:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\:sl-shadow-xl:hover{--tw-shadow:var(--shadow-xl);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.hover\:sl-shadow-2xl:hover{--tw-shadow:var(--shadow-2xl);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.focus\:sl-shadow-sm:focus{--tw-shadow:var(--shadow-sm);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.focus\:sl-shadow:focus{--tw-shadow:var(--shadow-md);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.focus\:sl-shadow-md:focus{--tw-shadow:var(--shadow-md);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.focus\:sl-shadow-lg:focus{--tw-shadow:var(--shadow-lg);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.focus\:sl-shadow-xl:focus{--tw-shadow:var(--shadow-xl);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.focus\:sl-shadow-2xl:focus{--tw-shadow:var(--shadow-2xl);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.sl-box-border{box-sizing:border-box}.sl-box-content{box-sizing:content-box}.sl-cursor-auto{cursor:auto}.sl-cursor{cursor:default}.sl-cursor-pointer{cursor:pointer}.sl-cursor-wait{cursor:wait}.sl-cursor-text{cursor:text}.sl-cursor-move{cursor:move}.sl-cursor-not-allowed{cursor:not-allowed}.sl-cursor-zoom-in{cursor:zoom-in}.sl-cursor-zoom-out{cursor:zoom-out}.sl-block{display:block}.sl-inline-block{display:inline-block}.sl-inline{display:inline}.sl-flex{display:flex}.sl-inline-flex{display:inline-flex}.sl-table{display:table}.sl-inline-table{display:inline-table}.sl-table-caption{display:table-caption}.sl-table-cell{display:table-cell}.sl-table-column{display:table-column}.sl-table-column-group{display:table-column-group}.sl-table-footer-group{display:table-footer-group}.sl-table-header-group{display:table-header-group}.sl-table-row-group{display:table-row-group}.sl-table-row{display:table-row}.sl-flow-root{display:flow-root}.sl-grid{display:grid}.sl-inline-grid{display:inline-grid}.sl-contents{display:contents}.sl-list-item{display:list-item}.sl-hidden{display:none}.sl-drop-shadow{--tw-drop-shadow:drop-shadow(var(--drop-shadow-default1)) drop-shadow(var(--drop-shadow-default2))}.sl-filter{--tw-blur:var(--tw-empty,/*!*/ /*!*/);--tw-brightness:var(--tw-empty,/*!*/ /*!*/);--tw-contrast:var(--tw-empty,/*!*/ /*!*/);--tw-grayscale:var(--tw-empty,/*!*/ /*!*/);--tw-hue-rotate:var(--tw-empty,/*!*/ /*!*/);--tw-invert:var(--tw-empty,/*!*/ /*!*/);--tw-saturate:var(--tw-empty,/*!*/ /*!*/);--tw-sepia:var(--tw-empty,/*!*/ /*!*/);--tw-drop-shadow:var(--tw-empty,/*!*/ /*!*/);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.sl-filter-none{filter:none}.sl-flex-1{flex:1 1}.sl-flex-auto{flex:1 1 auto}.sl-flex-initial{flex:0 1 auto}.sl-flex-none{flex:none}.sl-flex-row{flex-direction:row}.sl-flex-row-reverse{flex-direction:row-reverse}.sl-flex-col{flex-direction:column}.sl-flex-col-reverse{flex-direction:column-reverse}.sl-flex-grow-0{flex-grow:0}.sl-flex-grow{flex-grow:1}.sl-flex-shrink-0{flex-shrink:0}.sl-flex-shrink{flex-shrink:1}.sl-flex-wrap{flex-wrap:wrap}.sl-flex-wrap-reverse{flex-wrap:wrap-reverse}.sl-flex-nowrap{flex-wrap:nowrap}.sl-font-sans,.sl-font-ui{font-family:var(--font-ui)}.sl-font-prose{font-family:var(--font-prose)}.sl-font-mono{font-family:var(--font-mono)}.sl-text-2xs{font-size:9px}.sl-text-xs{font-size:10px}.sl-text-sm{font-size:11px}.sl-text-base{font-size:12px}.sl-text-lg{font-size:14px}.sl-text-xl{font-size:16px}.sl-text-2xl{font-size:20px}.sl-text-3xl{font-size:24px}.sl-text-4xl{font-size:28px}.sl-text-5xl{font-size:36px}.sl-text-6xl{font-size:44px}.sl-text-paragraph-leading{font-size:var(--fs-paragraph-leading)}.sl-text-paragraph{font-size:var(--fs-paragraph)}.sl-text-paragraph-small{font-size:var(--fs-paragraph-small)}.sl-text-paragraph-tiny{font-size:var(--fs-paragraph-tiny)}.sl-antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.sl-subpixel-antialiased{-webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto}.sl-italic{font-style:italic}.sl-not-italic{font-style:normal}.sl-font-light{font-weight:300}.sl-font-normal{font-weight:400}.sl-font-medium{font-weight:500}.sl-font-semibold{font-weight:600}.sl-font-bold{font-weight:700}.sl-h-0{height:0}.sl-h-1{height:4px}.sl-h-2{height:8px}.sl-h-3{height:12px}.sl-h-4{height:16px}.sl-h-5{height:20px}.sl-h-6{height:24px}.sl-h-7{height:28px}.sl-h-8{height:32px}.sl-h-9{height:36px}.sl-h-10{height:40px}.sl-h-11{height:44px}.sl-h-12{height:48px}.sl-h-14{height:56px}.sl-h-16{height:64px}.sl-h-20{height:80px}.sl-h-24{height:96px}.sl-h-28{height:112px}.sl-h-32{height:128px}.sl-h-36{height:144px}.sl-h-40{height:160px}.sl-h-44{height:176px}.sl-h-48{height:192px}.sl-h-52{height:208px}.sl-h-56{height:224px}.sl-h-60{height:240px}.sl-h-64{height:256px}.sl-h-72{height:288px}.sl-h-80{height:320px}.sl-h-96{height:384px}.sl-h-auto{height:auto}.sl-h-px{height:1px}.sl-h-0\.5{height:2px}.sl-h-1\.5{height:6px}.sl-h-2\.5{height:10px}.sl-h-3\.5{height:14px}.sl-h-4\.5{height:18px}.sl-h-xs{height:20px}.sl-h-sm{height:24px}.sl-h-md{height:32px}.sl-h-lg{height:36px}.sl-h-xl{height:44px}.sl-h-2xl{height:52px}.sl-h-3xl{height:60px}.sl-h-full{height:100%}.sl-h-screen{height:100vh}.sl-inset-0{bottom:0;left:0;right:0;top:0}.sl-inset-1{bottom:4px;left:4px;right:4px;top:4px}.sl-inset-2{bottom:8px;left:8px;right:8px;top:8px}.sl-inset-3{bottom:12px;left:12px;right:12px;top:12px}.sl-inset-4{bottom:16px;left:16px;right:16px;top:16px}.sl-inset-5{bottom:20px;left:20px;right:20px;top:20px}.sl-inset-6{bottom:24px;left:24px;right:24px;top:24px}.sl-inset-7{bottom:28px;left:28px;right:28px;top:28px}.sl-inset-8{bottom:32px;left:32px;right:32px;top:32px}.sl-inset-9{bottom:36px;left:36px;right:36px;top:36px}.sl-inset-10{bottom:40px;left:40px;right:40px;top:40px}.sl-inset-11{bottom:44px;left:44px;right:44px;top:44px}.sl-inset-12{bottom:48px;left:48px;right:48px;top:48px}.sl-inset-14{bottom:56px;left:56px;right:56px;top:56px}.sl-inset-16{bottom:64px;left:64px;right:64px;top:64px}.sl-inset-20{bottom:80px;left:80px;right:80px;top:80px}.sl-inset-24{bottom:96px;left:96px;right:96px;top:96px}.sl-inset-28{bottom:112px;left:112px;right:112px;top:112px}.sl-inset-32{bottom:128px;left:128px;right:128px;top:128px}.sl-inset-36{bottom:144px;left:144px;right:144px;top:144px}.sl-inset-40{bottom:160px;left:160px;right:160px;top:160px}.sl-inset-44{bottom:176px;left:176px;right:176px;top:176px}.sl-inset-48{bottom:192px;left:192px;right:192px;top:192px}.sl-inset-52{bottom:208px;left:208px;right:208px;top:208px}.sl-inset-56{bottom:224px;left:224px;right:224px;top:224px}.sl-inset-60{bottom:240px;left:240px;right:240px;top:240px}.sl-inset-64{bottom:256px;left:256px;right:256px;top:256px}.sl-inset-72{bottom:288px;left:288px;right:288px;top:288px}.sl-inset-80{bottom:320px;left:320px;right:320px;top:320px}.sl-inset-96{bottom:384px;left:384px;right:384px;top:384px}.sl-inset-auto{bottom:auto;left:auto;right:auto;top:auto}.sl-inset-px{bottom:1px;left:1px;right:1px;top:1px}.sl-inset-0\.5{bottom:2px;left:2px;right:2px;top:2px}.sl-inset-1\.5{bottom:6px;left:6px;right:6px;top:6px}.sl-inset-2\.5{bottom:10px;left:10px;right:10px;top:10px}.sl-inset-3\.5{bottom:14px;left:14px;right:14px;top:14px}.sl-inset-4\.5{bottom:18px;left:18px;right:18px;top:18px}.sl--inset-0{bottom:0;left:0;right:0;top:0}.sl--inset-1{bottom:-4px;left:-4px;right:-4px;top:-4px}.sl--inset-2{bottom:-8px;left:-8px;right:-8px;top:-8px}.sl--inset-3{bottom:-12px;left:-12px;right:-12px;top:-12px}.sl--inset-4{bottom:-16px;left:-16px;right:-16px;top:-16px}.sl--inset-5{bottom:-20px;left:-20px;right:-20px;top:-20px}.sl--inset-6{bottom:-24px;left:-24px;right:-24px;top:-24px}.sl--inset-7{bottom:-28px;left:-28px;right:-28px;top:-28px}.sl--inset-8{bottom:-32px;left:-32px;right:-32px;top:-32px}.sl--inset-9{bottom:-36px;left:-36px;right:-36px;top:-36px}.sl--inset-10{bottom:-40px;left:-40px;right:-40px;top:-40px}.sl--inset-11{bottom:-44px;left:-44px;right:-44px;top:-44px}.sl--inset-12{bottom:-48px;left:-48px;right:-48px;top:-48px}.sl--inset-14{bottom:-56px;left:-56px;right:-56px;top:-56px}.sl--inset-16{bottom:-64px;left:-64px;right:-64px;top:-64px}.sl--inset-20{bottom:-80px;left:-80px;right:-80px;top:-80px}.sl--inset-24{bottom:-96px;left:-96px;right:-96px;top:-96px}.sl--inset-28{bottom:-112px;left:-112px;right:-112px;top:-112px}.sl--inset-32{bottom:-128px;left:-128px;right:-128px;top:-128px}.sl--inset-36{bottom:-144px;left:-144px;right:-144px;top:-144px}.sl--inset-40{bottom:-160px;left:-160px;right:-160px;top:-160px}.sl--inset-44{bottom:-176px;left:-176px;right:-176px;top:-176px}.sl--inset-48{bottom:-192px;left:-192px;right:-192px;top:-192px}.sl--inset-52{bottom:-208px;left:-208px;right:-208px;top:-208px}.sl--inset-56{bottom:-224px;left:-224px;right:-224px;top:-224px}.sl--inset-60{bottom:-240px;left:-240px;right:-240px;top:-240px}.sl--inset-64{bottom:-256px;left:-256px;right:-256px;top:-256px}.sl--inset-72{bottom:-288px;left:-288px;right:-288px;top:-288px}.sl--inset-80{bottom:-320px;left:-320px;right:-320px;top:-320px}.sl--inset-96{bottom:-384px;left:-384px;right:-384px;top:-384px}.sl--inset-px{bottom:-1px;left:-1px;right:-1px;top:-1px}.sl--inset-0\.5{bottom:-2px;left:-2px;right:-2px;top:-2px}.sl--inset-1\.5{bottom:-6px;left:-6px;right:-6px;top:-6px}.sl--inset-2\.5{bottom:-10px;left:-10px;right:-10px;top:-10px}.sl--inset-3\.5{bottom:-14px;left:-14px;right:-14px;top:-14px}.sl--inset-4\.5{bottom:-18px;left:-18px;right:-18px;top:-18px}.sl-inset-y-0{bottom:0;top:0}.sl-inset-x-0{left:0;right:0}.sl-inset-y-1{bottom:4px;top:4px}.sl-inset-x-1{left:4px;right:4px}.sl-inset-y-2{bottom:8px;top:8px}.sl-inset-x-2{left:8px;right:8px}.sl-inset-y-3{bottom:12px;top:12px}.sl-inset-x-3{left:12px;right:12px}.sl-inset-y-4{bottom:16px;top:16px}.sl-inset-x-4{left:16px;right:16px}.sl-inset-y-5{bottom:20px;top:20px}.sl-inset-x-5{left:20px;right:20px}.sl-inset-y-6{bottom:24px;top:24px}.sl-inset-x-6{left:24px;right:24px}.sl-inset-y-7{bottom:28px;top:28px}.sl-inset-x-7{left:28px;right:28px}.sl-inset-y-8{bottom:32px;top:32px}.sl-inset-x-8{left:32px;right:32px}.sl-inset-y-9{bottom:36px;top:36px}.sl-inset-x-9{left:36px;right:36px}.sl-inset-y-10{bottom:40px;top:40px}.sl-inset-x-10{left:40px;right:40px}.sl-inset-y-11{bottom:44px;top:44px}.sl-inset-x-11{left:44px;right:44px}.sl-inset-y-12{bottom:48px;top:48px}.sl-inset-x-12{left:48px;right:48px}.sl-inset-y-14{bottom:56px;top:56px}.sl-inset-x-14{left:56px;right:56px}.sl-inset-y-16{bottom:64px;top:64px}.sl-inset-x-16{left:64px;right:64px}.sl-inset-y-20{bottom:80px;top:80px}.sl-inset-x-20{left:80px;right:80px}.sl-inset-y-24{bottom:96px;top:96px}.sl-inset-x-24{left:96px;right:96px}.sl-inset-y-28{bottom:112px;top:112px}.sl-inset-x-28{left:112px;right:112px}.sl-inset-y-32{bottom:128px;top:128px}.sl-inset-x-32{left:128px;right:128px}.sl-inset-y-36{bottom:144px;top:144px}.sl-inset-x-36{left:144px;right:144px}.sl-inset-y-40{bottom:160px;top:160px}.sl-inset-x-40{left:160px;right:160px}.sl-inset-y-44{bottom:176px;top:176px}.sl-inset-x-44{left:176px;right:176px}.sl-inset-y-48{bottom:192px;top:192px}.sl-inset-x-48{left:192px;right:192px}.sl-inset-y-52{bottom:208px;top:208px}.sl-inset-x-52{left:208px;right:208px}.sl-inset-y-56{bottom:224px;top:224px}.sl-inset-x-56{left:224px;right:224px}.sl-inset-y-60{bottom:240px;top:240px}.sl-inset-x-60{left:240px;right:240px}.sl-inset-y-64{bottom:256px;top:256px}.sl-inset-x-64{left:256px;right:256px}.sl-inset-y-72{bottom:288px;top:288px}.sl-inset-x-72{left:288px;right:288px}.sl-inset-y-80{bottom:320px;top:320px}.sl-inset-x-80{left:320px;right:320px}.sl-inset-y-96{bottom:384px;top:384px}.sl-inset-x-96{left:384px;right:384px}.sl-inset-y-auto{bottom:auto;top:auto}.sl-inset-x-auto{left:auto;right:auto}.sl-inset-y-px{bottom:1px;top:1px}.sl-inset-x-px{left:1px;right:1px}.sl-inset-y-0\.5{bottom:2px;top:2px}.sl-inset-x-0\.5{left:2px;right:2px}.sl-inset-y-1\.5{bottom:6px;top:6px}.sl-inset-x-1\.5{left:6px;right:6px}.sl-inset-y-2\.5{bottom:10px;top:10px}.sl-inset-x-2\.5{left:10px;right:10px}.sl-inset-y-3\.5{bottom:14px;top:14px}.sl-inset-x-3\.5{left:14px;right:14px}.sl-inset-y-4\.5{bottom:18px;top:18px}.sl-inset-x-4\.5{left:18px;right:18px}.sl--inset-y-0{bottom:0;top:0}.sl--inset-x-0{left:0;right:0}.sl--inset-y-1{bottom:-4px;top:-4px}.sl--inset-x-1{left:-4px;right:-4px}.sl--inset-y-2{bottom:-8px;top:-8px}.sl--inset-x-2{left:-8px;right:-8px}.sl--inset-y-3{bottom:-12px;top:-12px}.sl--inset-x-3{left:-12px;right:-12px}.sl--inset-y-4{bottom:-16px;top:-16px}.sl--inset-x-4{left:-16px;right:-16px}.sl--inset-y-5{bottom:-20px;top:-20px}.sl--inset-x-5{left:-20px;right:-20px}.sl--inset-y-6{bottom:-24px;top:-24px}.sl--inset-x-6{left:-24px;right:-24px}.sl--inset-y-7{bottom:-28px;top:-28px}.sl--inset-x-7{left:-28px;right:-28px}.sl--inset-y-8{bottom:-32px;top:-32px}.sl--inset-x-8{left:-32px;right:-32px}.sl--inset-y-9{bottom:-36px;top:-36px}.sl--inset-x-9{left:-36px;right:-36px}.sl--inset-y-10{bottom:-40px;top:-40px}.sl--inset-x-10{left:-40px;right:-40px}.sl--inset-y-11{bottom:-44px;top:-44px}.sl--inset-x-11{left:-44px;right:-44px}.sl--inset-y-12{bottom:-48px;top:-48px}.sl--inset-x-12{left:-48px;right:-48px}.sl--inset-y-14{bottom:-56px;top:-56px}.sl--inset-x-14{left:-56px;right:-56px}.sl--inset-y-16{bottom:-64px;top:-64px}.sl--inset-x-16{left:-64px;right:-64px}.sl--inset-y-20{bottom:-80px;top:-80px}.sl--inset-x-20{left:-80px;right:-80px}.sl--inset-y-24{bottom:-96px;top:-96px}.sl--inset-x-24{left:-96px;right:-96px}.sl--inset-y-28{bottom:-112px;top:-112px}.sl--inset-x-28{left:-112px;right:-112px}.sl--inset-y-32{bottom:-128px;top:-128px}.sl--inset-x-32{left:-128px;right:-128px}.sl--inset-y-36{bottom:-144px;top:-144px}.sl--inset-x-36{left:-144px;right:-144px}.sl--inset-y-40{bottom:-160px;top:-160px}.sl--inset-x-40{left:-160px;right:-160px}.sl--inset-y-44{bottom:-176px;top:-176px}.sl--inset-x-44{left:-176px;right:-176px}.sl--inset-y-48{bottom:-192px;top:-192px}.sl--inset-x-48{left:-192px;right:-192px}.sl--inset-y-52{bottom:-208px;top:-208px}.sl--inset-x-52{left:-208px;right:-208px}.sl--inset-y-56{bottom:-224px;top:-224px}.sl--inset-x-56{left:-224px;right:-224px}.sl--inset-y-60{bottom:-240px;top:-240px}.sl--inset-x-60{left:-240px;right:-240px}.sl--inset-y-64{bottom:-256px;top:-256px}.sl--inset-x-64{left:-256px;right:-256px}.sl--inset-y-72{bottom:-288px;top:-288px}.sl--inset-x-72{left:-288px;right:-288px}.sl--inset-y-80{bottom:-320px;top:-320px}.sl--inset-x-80{left:-320px;right:-320px}.sl--inset-y-96{bottom:-384px;top:-384px}.sl--inset-x-96{left:-384px;right:-384px}.sl--inset-y-px{bottom:-1px;top:-1px}.sl--inset-x-px{left:-1px;right:-1px}.sl--inset-y-0\.5{bottom:-2px;top:-2px}.sl--inset-x-0\.5{left:-2px;right:-2px}.sl--inset-y-1\.5{bottom:-6px;top:-6px}.sl--inset-x-1\.5{left:-6px;right:-6px}.sl--inset-y-2\.5{bottom:-10px;top:-10px}.sl--inset-x-2\.5{left:-10px;right:-10px}.sl--inset-y-3\.5{bottom:-14px;top:-14px}.sl--inset-x-3\.5{left:-14px;right:-14px}.sl--inset-y-4\.5{bottom:-18px;top:-18px}.sl--inset-x-4\.5{left:-18px;right:-18px}.sl-top-0{top:0}.sl-right-0{right:0}.sl-bottom-0{bottom:0}.sl-left-0{left:0}.sl-top-1{top:4px}.sl-right-1{right:4px}.sl-bottom-1{bottom:4px}.sl-left-1{left:4px}.sl-top-2{top:8px}.sl-right-2{right:8px}.sl-bottom-2{bottom:8px}.sl-left-2{left:8px}.sl-top-3{top:12px}.sl-right-3{right:12px}.sl-bottom-3{bottom:12px}.sl-left-3{left:12px}.sl-top-4{top:16px}.sl-right-4{right:16px}.sl-bottom-4{bottom:16px}.sl-left-4{left:16px}.sl-top-5{top:20px}.sl-right-5{right:20px}.sl-bottom-5{bottom:20px}.sl-left-5{left:20px}.sl-top-6{top:24px}.sl-right-6{right:24px}.sl-bottom-6{bottom:24px}.sl-left-6{left:24px}.sl-top-7{top:28px}.sl-right-7{right:28px}.sl-bottom-7{bottom:28px}.sl-left-7{left:28px}.sl-top-8{top:32px}.sl-right-8{right:32px}.sl-bottom-8{bottom:32px}.sl-left-8{left:32px}.sl-top-9{top:36px}.sl-right-9{right:36px}.sl-bottom-9{bottom:36px}.sl-left-9{left:36px}.sl-top-10{top:40px}.sl-right-10{right:40px}.sl-bottom-10{bottom:40px}.sl-left-10{left:40px}.sl-top-11{top:44px}.sl-right-11{right:44px}.sl-bottom-11{bottom:44px}.sl-left-11{left:44px}.sl-top-12{top:48px}.sl-right-12{right:48px}.sl-bottom-12{bottom:48px}.sl-left-12{left:48px}.sl-top-14{top:56px}.sl-right-14{right:56px}.sl-bottom-14{bottom:56px}.sl-left-14{left:56px}.sl-top-16{top:64px}.sl-right-16{right:64px}.sl-bottom-16{bottom:64px}.sl-left-16{left:64px}.sl-top-20{top:80px}.sl-right-20{right:80px}.sl-bottom-20{bottom:80px}.sl-left-20{left:80px}.sl-top-24{top:96px}.sl-right-24{right:96px}.sl-bottom-24{bottom:96px}.sl-left-24{left:96px}.sl-top-28{top:112px}.sl-right-28{right:112px}.sl-bottom-28{bottom:112px}.sl-left-28{left:112px}.sl-top-32{top:128px}.sl-right-32{right:128px}.sl-bottom-32{bottom:128px}.sl-left-32{left:128px}.sl-top-36{top:144px}.sl-right-36{right:144px}.sl-bottom-36{bottom:144px}.sl-left-36{left:144px}.sl-top-40{top:160px}.sl-right-40{right:160px}.sl-bottom-40{bottom:160px}.sl-left-40{left:160px}.sl-top-44{top:176px}.sl-right-44{right:176px}.sl-bottom-44{bottom:176px}.sl-left-44{left:176px}.sl-top-48{top:192px}.sl-right-48{right:192px}.sl-bottom-48{bottom:192px}.sl-left-48{left:192px}.sl-top-52{top:208px}.sl-right-52{right:208px}.sl-bottom-52{bottom:208px}.sl-left-52{left:208px}.sl-top-56{top:224px}.sl-right-56{right:224px}.sl-bottom-56{bottom:224px}.sl-left-56{left:224px}.sl-top-60{top:240px}.sl-right-60{right:240px}.sl-bottom-60{bottom:240px}.sl-left-60{left:240px}.sl-top-64{top:256px}.sl-right-64{right:256px}.sl-bottom-64{bottom:256px}.sl-left-64{left:256px}.sl-top-72{top:288px}.sl-right-72{right:288px}.sl-bottom-72{bottom:288px}.sl-left-72{left:288px}.sl-top-80{top:320px}.sl-right-80{right:320px}.sl-bottom-80{bottom:320px}.sl-left-80{left:320px}.sl-top-96{top:384px}.sl-right-96{right:384px}.sl-bottom-96{bottom:384px}.sl-left-96{left:384px}.sl-top-auto{top:auto}.sl-right-auto{right:auto}.sl-bottom-auto{bottom:auto}.sl-left-auto{left:auto}.sl-top-px{top:1px}.sl-right-px{right:1px}.sl-bottom-px{bottom:1px}.sl-left-px{left:1px}.sl-top-0\.5{top:2px}.sl-right-0\.5{right:2px}.sl-bottom-0\.5{bottom:2px}.sl-left-0\.5{left:2px}.sl-top-1\.5{top:6px}.sl-right-1\.5{right:6px}.sl-bottom-1\.5{bottom:6px}.sl-left-1\.5{left:6px}.sl-top-2\.5{top:10px}.sl-right-2\.5{right:10px}.sl-bottom-2\.5{bottom:10px}.sl-left-2\.5{left:10px}.sl-top-3\.5{top:14px}.sl-right-3\.5{right:14px}.sl-bottom-3\.5{bottom:14px}.sl-left-3\.5{left:14px}.sl-top-4\.5{top:18px}.sl-right-4\.5{right:18px}.sl-bottom-4\.5{bottom:18px}.sl-left-4\.5{left:18px}.sl--top-0{top:0}.sl--right-0{right:0}.sl--bottom-0{bottom:0}.sl--left-0{left:0}.sl--top-1{top:-4px}.sl--right-1{right:-4px}.sl--bottom-1{bottom:-4px}.sl--left-1{left:-4px}.sl--top-2{top:-8px}.sl--right-2{right:-8px}.sl--bottom-2{bottom:-8px}.sl--left-2{left:-8px}.sl--top-3{top:-12px}.sl--right-3{right:-12px}.sl--bottom-3{bottom:-12px}.sl--left-3{left:-12px}.sl--top-4{top:-16px}.sl--right-4{right:-16px}.sl--bottom-4{bottom:-16px}.sl--left-4{left:-16px}.sl--top-5{top:-20px}.sl--right-5{right:-20px}.sl--bottom-5{bottom:-20px}.sl--left-5{left:-20px}.sl--top-6{top:-24px}.sl--right-6{right:-24px}.sl--bottom-6{bottom:-24px}.sl--left-6{left:-24px}.sl--top-7{top:-28px}.sl--right-7{right:-28px}.sl--bottom-7{bottom:-28px}.sl--left-7{left:-28px}.sl--top-8{top:-32px}.sl--right-8{right:-32px}.sl--bottom-8{bottom:-32px}.sl--left-8{left:-32px}.sl--top-9{top:-36px}.sl--right-9{right:-36px}.sl--bottom-9{bottom:-36px}.sl--left-9{left:-36px}.sl--top-10{top:-40px}.sl--right-10{right:-40px}.sl--bottom-10{bottom:-40px}.sl--left-10{left:-40px}.sl--top-11{top:-44px}.sl--right-11{right:-44px}.sl--bottom-11{bottom:-44px}.sl--left-11{left:-44px}.sl--top-12{top:-48px}.sl--right-12{right:-48px}.sl--bottom-12{bottom:-48px}.sl--left-12{left:-48px}.sl--top-14{top:-56px}.sl--right-14{right:-56px}.sl--bottom-14{bottom:-56px}.sl--left-14{left:-56px}.sl--top-16{top:-64px}.sl--right-16{right:-64px}.sl--bottom-16{bottom:-64px}.sl--left-16{left:-64px}.sl--top-20{top:-80px}.sl--right-20{right:-80px}.sl--bottom-20{bottom:-80px}.sl--left-20{left:-80px}.sl--top-24{top:-96px}.sl--right-24{right:-96px}.sl--bottom-24{bottom:-96px}.sl--left-24{left:-96px}.sl--top-28{top:-112px}.sl--right-28{right:-112px}.sl--bottom-28{bottom:-112px}.sl--left-28{left:-112px}.sl--top-32{top:-128px}.sl--right-32{right:-128px}.sl--bottom-32{bottom:-128px}.sl--left-32{left:-128px}.sl--top-36{top:-144px}.sl--right-36{right:-144px}.sl--bottom-36{bottom:-144px}.sl--left-36{left:-144px}.sl--top-40{top:-160px}.sl--right-40{right:-160px}.sl--bottom-40{bottom:-160px}.sl--left-40{left:-160px}.sl--top-44{top:-176px}.sl--right-44{right:-176px}.sl--bottom-44{bottom:-176px}.sl--left-44{left:-176px}.sl--top-48{top:-192px}.sl--right-48{right:-192px}.sl--bottom-48{bottom:-192px}.sl--left-48{left:-192px}.sl--top-52{top:-208px}.sl--right-52{right:-208px}.sl--bottom-52{bottom:-208px}.sl--left-52{left:-208px}.sl--top-56{top:-224px}.sl--right-56{right:-224px}.sl--bottom-56{bottom:-224px}.sl--left-56{left:-224px}.sl--top-60{top:-240px}.sl--right-60{right:-240px}.sl--bottom-60{bottom:-240px}.sl--left-60{left:-240px}.sl--top-64{top:-256px}.sl--right-64{right:-256px}.sl--bottom-64{bottom:-256px}.sl--left-64{left:-256px}.sl--top-72{top:-288px}.sl--right-72{right:-288px}.sl--bottom-72{bottom:-288px}.sl--left-72{left:-288px}.sl--top-80{top:-320px}.sl--right-80{right:-320px}.sl--bottom-80{bottom:-320px}.sl--left-80{left:-320px}.sl--top-96{top:-384px}.sl--right-96{right:-384px}.sl--bottom-96{bottom:-384px}.sl--left-96{left:-384px}.sl--top-px{top:-1px}.sl--right-px{right:-1px}.sl--bottom-px{bottom:-1px}.sl--left-px{left:-1px}.sl--top-0\.5{top:-2px}.sl--right-0\.5{right:-2px}.sl--bottom-0\.5{bottom:-2px}.sl--left-0\.5{left:-2px}.sl--top-1\.5{top:-6px}.sl--right-1\.5{right:-6px}.sl--bottom-1\.5{bottom:-6px}.sl--left-1\.5{left:-6px}.sl--top-2\.5{top:-10px}.sl--right-2\.5{right:-10px}.sl--bottom-2\.5{bottom:-10px}.sl--left-2\.5{left:-10px}.sl--top-3\.5{top:-14px}.sl--right-3\.5{right:-14px}.sl--bottom-3\.5{bottom:-14px}.sl--left-3\.5{left:-14px}.sl--top-4\.5{top:-18px}.sl--right-4\.5{right:-18px}.sl--bottom-4\.5{bottom:-18px}.sl--left-4\.5{left:-18px}.sl-justify-start{justify-content:flex-start}.sl-justify-end{justify-content:flex-end}.sl-justify-center{justify-content:center}.sl-justify-between{justify-content:space-between}.sl-justify-around{justify-content:space-around}.sl-justify-evenly{justify-content:space-evenly}.sl-justify-items-start{justify-items:start}.sl-justify-items-end{justify-items:end}.sl-justify-items-center{justify-items:center}.sl-justify-items-stretch{justify-items:stretch}.sl-justify-self-auto{justify-self:auto}.sl-justify-self-start{justify-self:start}.sl-justify-self-end{justify-self:end}.sl-justify-self-center{justify-self:center}.sl-justify-self-stretch{justify-self:stretch}.sl-tracking-tight{letter-spacing:-.025em}.sl-tracking-normal{letter-spacing:0}.sl-tracking-wide{letter-spacing:.025em}.sl-leading-none{line-height:1}.sl-leading-tight{line-height:1.2}.sl-leading-snug{line-height:1.375}.sl-leading-normal{line-height:1.5}.sl-leading-relaxed{line-height:1.625}.sl-leading-loose{line-height:2}.sl-leading-paragraph-leading{line-height:var(--lh-paragraph-leading)}.sl-leading-paragraph{line-height:var(--lh-paragraph)}.sl-leading-paragraph-small{line-height:var(--lh-paragraph-small)}.sl-leading-paragraph-tiny{line-height:var(--lh-paragraph-tiny)}.sl-m-0{margin:0}.sl-m-1{margin:4px}.sl-m-2{margin:8px}.sl-m-3{margin:12px}.sl-m-4{margin:16px}.sl-m-5{margin:20px}.sl-m-6{margin:24px}.sl-m-7{margin:28px}.sl-m-8{margin:32px}.sl-m-9{margin:36px}.sl-m-10{margin:40px}.sl-m-11{margin:44px}.sl-m-12{margin:48px}.sl-m-14{margin:56px}.sl-m-16{margin:64px}.sl-m-20{margin:80px}.sl-m-24{margin:96px}.sl-m-28{margin:112px}.sl-m-32{margin:128px}.sl-m-36{margin:144px}.sl-m-40{margin:160px}.sl-m-44{margin:176px}.sl-m-48{margin:192px}.sl-m-52{margin:208px}.sl-m-56{margin:224px}.sl-m-60{margin:240px}.sl-m-64{margin:256px}.sl-m-72{margin:288px}.sl-m-80{margin:320px}.sl-m-96{margin:384px}.sl-m-auto{margin:auto}.sl-m-px{margin:1px}.sl-m-0\.5{margin:2px}.sl-m-1\.5{margin:6px}.sl-m-2\.5{margin:10px}.sl-m-3\.5{margin:14px}.sl-m-4\.5{margin:18px}.sl--m-0{margin:0}.sl--m-1{margin:-4px}.sl--m-2{margin:-8px}.sl--m-3{margin:-12px}.sl--m-4{margin:-16px}.sl--m-5{margin:-20px}.sl--m-6{margin:-24px}.sl--m-7{margin:-28px}.sl--m-8{margin:-32px}.sl--m-9{margin:-36px}.sl--m-10{margin:-40px}.sl--m-11{margin:-44px}.sl--m-12{margin:-48px}.sl--m-14{margin:-56px}.sl--m-16{margin:-64px}.sl--m-20{margin:-80px}.sl--m-24{margin:-96px}.sl--m-28{margin:-112px}.sl--m-32{margin:-128px}.sl--m-36{margin:-144px}.sl--m-40{margin:-160px}.sl--m-44{margin:-176px}.sl--m-48{margin:-192px}.sl--m-52{margin:-208px}.sl--m-56{margin:-224px}.sl--m-60{margin:-240px}.sl--m-64{margin:-256px}.sl--m-72{margin:-288px}.sl--m-80{margin:-320px}.sl--m-96{margin:-384px}.sl--m-px{margin:-1px}.sl--m-0\.5{margin:-2px}.sl--m-1\.5{margin:-6px}.sl--m-2\.5{margin:-10px}.sl--m-3\.5{margin:-14px}.sl--m-4\.5{margin:-18px}.sl-my-0{margin-bottom:0;margin-top:0}.sl-mx-0{margin-left:0;margin-right:0}.sl-my-1{margin-bottom:4px;margin-top:4px}.sl-mx-1{margin-left:4px;margin-right:4px}.sl-my-2{margin-bottom:8px;margin-top:8px}.sl-mx-2{margin-left:8px;margin-right:8px}.sl-my-3{margin-bottom:12px;margin-top:12px}.sl-mx-3{margin-left:12px;margin-right:12px}.sl-my-4{margin-bottom:16px;margin-top:16px}.sl-mx-4{margin-left:16px;margin-right:16px}.sl-my-5{margin-bottom:20px;margin-top:20px}.sl-mx-5{margin-left:20px;margin-right:20px}.sl-my-6{margin-bottom:24px;margin-top:24px}.sl-mx-6{margin-left:24px;margin-right:24px}.sl-my-7{margin-bottom:28px;margin-top:28px}.sl-mx-7{margin-left:28px;margin-right:28px}.sl-my-8{margin-bottom:32px;margin-top:32px}.sl-mx-8{margin-left:32px;margin-right:32px}.sl-my-9{margin-bottom:36px;margin-top:36px}.sl-mx-9{margin-left:36px;margin-right:36px}.sl-my-10{margin-bottom:40px;margin-top:40px}.sl-mx-10{margin-left:40px;margin-right:40px}.sl-my-11{margin-bottom:44px;margin-top:44px}.sl-mx-11{margin-left:44px;margin-right:44px}.sl-my-12{margin-bottom:48px;margin-top:48px}.sl-mx-12{margin-left:48px;margin-right:48px}.sl-my-14{margin-bottom:56px;margin-top:56px}.sl-mx-14{margin-left:56px;margin-right:56px}.sl-my-16{margin-bottom:64px;margin-top:64px}.sl-mx-16{margin-left:64px;margin-right:64px}.sl-my-20{margin-bottom:80px;margin-top:80px}.sl-mx-20{margin-left:80px;margin-right:80px}.sl-my-24{margin-bottom:96px;margin-top:96px}.sl-mx-24{margin-left:96px;margin-right:96px}.sl-my-28{margin-bottom:112px;margin-top:112px}.sl-mx-28{margin-left:112px;margin-right:112px}.sl-my-32{margin-bottom:128px;margin-top:128px}.sl-mx-32{margin-left:128px;margin-right:128px}.sl-my-36{margin-bottom:144px;margin-top:144px}.sl-mx-36{margin-left:144px;margin-right:144px}.sl-my-40{margin-bottom:160px;margin-top:160px}.sl-mx-40{margin-left:160px;margin-right:160px}.sl-my-44{margin-bottom:176px;margin-top:176px}.sl-mx-44{margin-left:176px;margin-right:176px}.sl-my-48{margin-bottom:192px;margin-top:192px}.sl-mx-48{margin-left:192px;margin-right:192px}.sl-my-52{margin-bottom:208px;margin-top:208px}.sl-mx-52{margin-left:208px;margin-right:208px}.sl-my-56{margin-bottom:224px;margin-top:224px}.sl-mx-56{margin-left:224px;margin-right:224px}.sl-my-60{margin-bottom:240px;margin-top:240px}.sl-mx-60{margin-left:240px;margin-right:240px}.sl-my-64{margin-bottom:256px;margin-top:256px}.sl-mx-64{margin-left:256px;margin-right:256px}.sl-my-72{margin-bottom:288px;margin-top:288px}.sl-mx-72{margin-left:288px;margin-right:288px}.sl-my-80{margin-bottom:320px;margin-top:320px}.sl-mx-80{margin-left:320px;margin-right:320px}.sl-my-96{margin-bottom:384px;margin-top:384px}.sl-mx-96{margin-left:384px;margin-right:384px}.sl-my-auto{margin-bottom:auto;margin-top:auto}.sl-mx-auto{margin-left:auto;margin-right:auto}.sl-my-px{margin-bottom:1px;margin-top:1px}.sl-mx-px{margin-left:1px;margin-right:1px}.sl-my-0\.5{margin-bottom:2px;margin-top:2px}.sl-mx-0\.5{margin-left:2px;margin-right:2px}.sl-my-1\.5{margin-bottom:6px;margin-top:6px}.sl-mx-1\.5{margin-left:6px;margin-right:6px}.sl-my-2\.5{margin-bottom:10px;margin-top:10px}.sl-mx-2\.5{margin-left:10px;margin-right:10px}.sl-my-3\.5{margin-bottom:14px;margin-top:14px}.sl-mx-3\.5{margin-left:14px;margin-right:14px}.sl-my-4\.5{margin-bottom:18px;margin-top:18px}.sl-mx-4\.5{margin-left:18px;margin-right:18px}.sl--my-0{margin-bottom:0;margin-top:0}.sl--mx-0{margin-left:0;margin-right:0}.sl--my-1{margin-bottom:-4px;margin-top:-4px}.sl--mx-1{margin-left:-4px;margin-right:-4px}.sl--my-2{margin-bottom:-8px;margin-top:-8px}.sl--mx-2{margin-left:-8px;margin-right:-8px}.sl--my-3{margin-bottom:-12px;margin-top:-12px}.sl--mx-3{margin-left:-12px;margin-right:-12px}.sl--my-4{margin-bottom:-16px;margin-top:-16px}.sl--mx-4{margin-left:-16px;margin-right:-16px}.sl--my-5{margin-bottom:-20px;margin-top:-20px}.sl--mx-5{margin-left:-20px;margin-right:-20px}.sl--my-6{margin-bottom:-24px;margin-top:-24px}.sl--mx-6{margin-left:-24px;margin-right:-24px}.sl--my-7{margin-bottom:-28px;margin-top:-28px}.sl--mx-7{margin-left:-28px;margin-right:-28px}.sl--my-8{margin-bottom:-32px;margin-top:-32px}.sl--mx-8{margin-left:-32px;margin-right:-32px}.sl--my-9{margin-bottom:-36px;margin-top:-36px}.sl--mx-9{margin-left:-36px;margin-right:-36px}.sl--my-10{margin-bottom:-40px;margin-top:-40px}.sl--mx-10{margin-left:-40px;margin-right:-40px}.sl--my-11{margin-bottom:-44px;margin-top:-44px}.sl--mx-11{margin-left:-44px;margin-right:-44px}.sl--my-12{margin-bottom:-48px;margin-top:-48px}.sl--mx-12{margin-left:-48px;margin-right:-48px}.sl--my-14{margin-bottom:-56px;margin-top:-56px}.sl--mx-14{margin-left:-56px;margin-right:-56px}.sl--my-16{margin-bottom:-64px;margin-top:-64px}.sl--mx-16{margin-left:-64px;margin-right:-64px}.sl--my-20{margin-bottom:-80px;margin-top:-80px}.sl--mx-20{margin-left:-80px;margin-right:-80px}.sl--my-24{margin-bottom:-96px;margin-top:-96px}.sl--mx-24{margin-left:-96px;margin-right:-96px}.sl--my-28{margin-bottom:-112px;margin-top:-112px}.sl--mx-28{margin-left:-112px;margin-right:-112px}.sl--my-32{margin-bottom:-128px;margin-top:-128px}.sl--mx-32{margin-left:-128px;margin-right:-128px}.sl--my-36{margin-bottom:-144px;margin-top:-144px}.sl--mx-36{margin-left:-144px;margin-right:-144px}.sl--my-40{margin-bottom:-160px;margin-top:-160px}.sl--mx-40{margin-left:-160px;margin-right:-160px}.sl--my-44{margin-bottom:-176px;margin-top:-176px}.sl--mx-44{margin-left:-176px;margin-right:-176px}.sl--my-48{margin-bottom:-192px;margin-top:-192px}.sl--mx-48{margin-left:-192px;margin-right:-192px}.sl--my-52{margin-bottom:-208px;margin-top:-208px}.sl--mx-52{margin-left:-208px;margin-right:-208px}.sl--my-56{margin-bottom:-224px;margin-top:-224px}.sl--mx-56{margin-left:-224px;margin-right:-224px}.sl--my-60{margin-bottom:-240px;margin-top:-240px}.sl--mx-60{margin-left:-240px;margin-right:-240px}.sl--my-64{margin-bottom:-256px;margin-top:-256px}.sl--mx-64{margin-left:-256px;margin-right:-256px}.sl--my-72{margin-bottom:-288px;margin-top:-288px}.sl--mx-72{margin-left:-288px;margin-right:-288px}.sl--my-80{margin-bottom:-320px;margin-top:-320px}.sl--mx-80{margin-left:-320px;margin-right:-320px}.sl--my-96{margin-bottom:-384px;margin-top:-384px}.sl--mx-96{margin-left:-384px;margin-right:-384px}.sl--my-px{margin-bottom:-1px;margin-top:-1px}.sl--mx-px{margin-left:-1px;margin-right:-1px}.sl--my-0\.5{margin-bottom:-2px;margin-top:-2px}.sl--mx-0\.5{margin-left:-2px;margin-right:-2px}.sl--my-1\.5{margin-bottom:-6px;margin-top:-6px}.sl--mx-1\.5{margin-left:-6px;margin-right:-6px}.sl--my-2\.5{margin-bottom:-10px;margin-top:-10px}.sl--mx-2\.5{margin-left:-10px;margin-right:-10px}.sl--my-3\.5{margin-bottom:-14px;margin-top:-14px}.sl--mx-3\.5{margin-left:-14px;margin-right:-14px}.sl--my-4\.5{margin-bottom:-18px;margin-top:-18px}.sl--mx-4\.5{margin-left:-18px;margin-right:-18px}.sl-mt-0{margin-top:0}.sl-mr-0{margin-right:0}.sl-mb-0{margin-bottom:0}.sl-ml-0{margin-left:0}.sl-mt-1{margin-top:4px}.sl-mr-1{margin-right:4px}.sl-mb-1{margin-bottom:4px}.sl-ml-1{margin-left:4px}.sl-mt-2{margin-top:8px}.sl-mr-2{margin-right:8px}.sl-mb-2{margin-bottom:8px}.sl-ml-2{margin-left:8px}.sl-mt-3{margin-top:12px}.sl-mr-3{margin-right:12px}.sl-mb-3{margin-bottom:12px}.sl-ml-3{margin-left:12px}.sl-mt-4{margin-top:16px}.sl-mr-4{margin-right:16px}.sl-mb-4{margin-bottom:16px}.sl-ml-4{margin-left:16px}.sl-mt-5{margin-top:20px}.sl-mr-5{margin-right:20px}.sl-mb-5{margin-bottom:20px}.sl-ml-5{margin-left:20px}.sl-mt-6{margin-top:24px}.sl-mr-6{margin-right:24px}.sl-mb-6{margin-bottom:24px}.sl-ml-6{margin-left:24px}.sl-mt-7{margin-top:28px}.sl-mr-7{margin-right:28px}.sl-mb-7{margin-bottom:28px}.sl-ml-7{margin-left:28px}.sl-mt-8{margin-top:32px}.sl-mr-8{margin-right:32px}.sl-mb-8{margin-bottom:32px}.sl-ml-8{margin-left:32px}.sl-mt-9{margin-top:36px}.sl-mr-9{margin-right:36px}.sl-mb-9{margin-bottom:36px}.sl-ml-9{margin-left:36px}.sl-mt-10{margin-top:40px}.sl-mr-10{margin-right:40px}.sl-mb-10{margin-bottom:40px}.sl-ml-10{margin-left:40px}.sl-mt-11{margin-top:44px}.sl-mr-11{margin-right:44px}.sl-mb-11{margin-bottom:44px}.sl-ml-11{margin-left:44px}.sl-mt-12{margin-top:48px}.sl-mr-12{margin-right:48px}.sl-mb-12{margin-bottom:48px}.sl-ml-12{margin-left:48px}.sl-mt-14{margin-top:56px}.sl-mr-14{margin-right:56px}.sl-mb-14{margin-bottom:56px}.sl-ml-14{margin-left:56px}.sl-mt-16{margin-top:64px}.sl-mr-16{margin-right:64px}.sl-mb-16{margin-bottom:64px}.sl-ml-16{margin-left:64px}.sl-mt-20{margin-top:80px}.sl-mr-20{margin-right:80px}.sl-mb-20{margin-bottom:80px}.sl-ml-20{margin-left:80px}.sl-mt-24{margin-top:96px}.sl-mr-24{margin-right:96px}.sl-mb-24{margin-bottom:96px}.sl-ml-24{margin-left:96px}.sl-mt-28{margin-top:112px}.sl-mr-28{margin-right:112px}.sl-mb-28{margin-bottom:112px}.sl-ml-28{margin-left:112px}.sl-mt-32{margin-top:128px}.sl-mr-32{margin-right:128px}.sl-mb-32{margin-bottom:128px}.sl-ml-32{margin-left:128px}.sl-mt-36{margin-top:144px}.sl-mr-36{margin-right:144px}.sl-mb-36{margin-bottom:144px}.sl-ml-36{margin-left:144px}.sl-mt-40{margin-top:160px}.sl-mr-40{margin-right:160px}.sl-mb-40{margin-bottom:160px}.sl-ml-40{margin-left:160px}.sl-mt-44{margin-top:176px}.sl-mr-44{margin-right:176px}.sl-mb-44{margin-bottom:176px}.sl-ml-44{margin-left:176px}.sl-mt-48{margin-top:192px}.sl-mr-48{margin-right:192px}.sl-mb-48{margin-bottom:192px}.sl-ml-48{margin-left:192px}.sl-mt-52{margin-top:208px}.sl-mr-52{margin-right:208px}.sl-mb-52{margin-bottom:208px}.sl-ml-52{margin-left:208px}.sl-mt-56{margin-top:224px}.sl-mr-56{margin-right:224px}.sl-mb-56{margin-bottom:224px}.sl-ml-56{margin-left:224px}.sl-mt-60{margin-top:240px}.sl-mr-60{margin-right:240px}.sl-mb-60{margin-bottom:240px}.sl-ml-60{margin-left:240px}.sl-mt-64{margin-top:256px}.sl-mr-64{margin-right:256px}.sl-mb-64{margin-bottom:256px}.sl-ml-64{margin-left:256px}.sl-mt-72{margin-top:288px}.sl-mr-72{margin-right:288px}.sl-mb-72{margin-bottom:288px}.sl-ml-72{margin-left:288px}.sl-mt-80{margin-top:320px}.sl-mr-80{margin-right:320px}.sl-mb-80{margin-bottom:320px}.sl-ml-80{margin-left:320px}.sl-mt-96{margin-top:384px}.sl-mr-96{margin-right:384px}.sl-mb-96{margin-bottom:384px}.sl-ml-96{margin-left:384px}.sl-mt-auto{margin-top:auto}.sl-mr-auto{margin-right:auto}.sl-mb-auto{margin-bottom:auto}.sl-ml-auto{margin-left:auto}.sl-mt-px{margin-top:1px}.sl-mr-px{margin-right:1px}.sl-mb-px{margin-bottom:1px}.sl-ml-px{margin-left:1px}.sl-mt-0\.5{margin-top:2px}.sl-mr-0\.5{margin-right:2px}.sl-mb-0\.5{margin-bottom:2px}.sl-ml-0\.5{margin-left:2px}.sl-mt-1\.5{margin-top:6px}.sl-mr-1\.5{margin-right:6px}.sl-mb-1\.5{margin-bottom:6px}.sl-ml-1\.5{margin-left:6px}.sl-mt-2\.5{margin-top:10px}.sl-mr-2\.5{margin-right:10px}.sl-mb-2\.5{margin-bottom:10px}.sl-ml-2\.5{margin-left:10px}.sl-mt-3\.5{margin-top:14px}.sl-mr-3\.5{margin-right:14px}.sl-mb-3\.5{margin-bottom:14px}.sl-ml-3\.5{margin-left:14px}.sl-mt-4\.5{margin-top:18px}.sl-mr-4\.5{margin-right:18px}.sl-mb-4\.5{margin-bottom:18px}.sl-ml-4\.5{margin-left:18px}.sl--mt-0{margin-top:0}.sl--mr-0{margin-right:0}.sl--mb-0{margin-bottom:0}.sl--ml-0{margin-left:0}.sl--mt-1{margin-top:-4px}.sl--mr-1{margin-right:-4px}.sl--mb-1{margin-bottom:-4px}.sl--ml-1{margin-left:-4px}.sl--mt-2{margin-top:-8px}.sl--mr-2{margin-right:-8px}.sl--mb-2{margin-bottom:-8px}.sl--ml-2{margin-left:-8px}.sl--mt-3{margin-top:-12px}.sl--mr-3{margin-right:-12px}.sl--mb-3{margin-bottom:-12px}.sl--ml-3{margin-left:-12px}.sl--mt-4{margin-top:-16px}.sl--mr-4{margin-right:-16px}.sl--mb-4{margin-bottom:-16px}.sl--ml-4{margin-left:-16px}.sl--mt-5{margin-top:-20px}.sl--mr-5{margin-right:-20px}.sl--mb-5{margin-bottom:-20px}.sl--ml-5{margin-left:-20px}.sl--mt-6{margin-top:-24px}.sl--mr-6{margin-right:-24px}.sl--mb-6{margin-bottom:-24px}.sl--ml-6{margin-left:-24px}.sl--mt-7{margin-top:-28px}.sl--mr-7{margin-right:-28px}.sl--mb-7{margin-bottom:-28px}.sl--ml-7{margin-left:-28px}.sl--mt-8{margin-top:-32px}.sl--mr-8{margin-right:-32px}.sl--mb-8{margin-bottom:-32px}.sl--ml-8{margin-left:-32px}.sl--mt-9{margin-top:-36px}.sl--mr-9{margin-right:-36px}.sl--mb-9{margin-bottom:-36px}.sl--ml-9{margin-left:-36px}.sl--mt-10{margin-top:-40px}.sl--mr-10{margin-right:-40px}.sl--mb-10{margin-bottom:-40px}.sl--ml-10{margin-left:-40px}.sl--mt-11{margin-top:-44px}.sl--mr-11{margin-right:-44px}.sl--mb-11{margin-bottom:-44px}.sl--ml-11{margin-left:-44px}.sl--mt-12{margin-top:-48px}.sl--mr-12{margin-right:-48px}.sl--mb-12{margin-bottom:-48px}.sl--ml-12{margin-left:-48px}.sl--mt-14{margin-top:-56px}.sl--mr-14{margin-right:-56px}.sl--mb-14{margin-bottom:-56px}.sl--ml-14{margin-left:-56px}.sl--mt-16{margin-top:-64px}.sl--mr-16{margin-right:-64px}.sl--mb-16{margin-bottom:-64px}.sl--ml-16{margin-left:-64px}.sl--mt-20{margin-top:-80px}.sl--mr-20{margin-right:-80px}.sl--mb-20{margin-bottom:-80px}.sl--ml-20{margin-left:-80px}.sl--mt-24{margin-top:-96px}.sl--mr-24{margin-right:-96px}.sl--mb-24{margin-bottom:-96px}.sl--ml-24{margin-left:-96px}.sl--mt-28{margin-top:-112px}.sl--mr-28{margin-right:-112px}.sl--mb-28{margin-bottom:-112px}.sl--ml-28{margin-left:-112px}.sl--mt-32{margin-top:-128px}.sl--mr-32{margin-right:-128px}.sl--mb-32{margin-bottom:-128px}.sl--ml-32{margin-left:-128px}.sl--mt-36{margin-top:-144px}.sl--mr-36{margin-right:-144px}.sl--mb-36{margin-bottom:-144px}.sl--ml-36{margin-left:-144px}.sl--mt-40{margin-top:-160px}.sl--mr-40{margin-right:-160px}.sl--mb-40{margin-bottom:-160px}.sl--ml-40{margin-left:-160px}.sl--mt-44{margin-top:-176px}.sl--mr-44{margin-right:-176px}.sl--mb-44{margin-bottom:-176px}.sl--ml-44{margin-left:-176px}.sl--mt-48{margin-top:-192px}.sl--mr-48{margin-right:-192px}.sl--mb-48{margin-bottom:-192px}.sl--ml-48{margin-left:-192px}.sl--mt-52{margin-top:-208px}.sl--mr-52{margin-right:-208px}.sl--mb-52{margin-bottom:-208px}.sl--ml-52{margin-left:-208px}.sl--mt-56{margin-top:-224px}.sl--mr-56{margin-right:-224px}.sl--mb-56{margin-bottom:-224px}.sl--ml-56{margin-left:-224px}.sl--mt-60{margin-top:-240px}.sl--mr-60{margin-right:-240px}.sl--mb-60{margin-bottom:-240px}.sl--ml-60{margin-left:-240px}.sl--mt-64{margin-top:-256px}.sl--mr-64{margin-right:-256px}.sl--mb-64{margin-bottom:-256px}.sl--ml-64{margin-left:-256px}.sl--mt-72{margin-top:-288px}.sl--mr-72{margin-right:-288px}.sl--mb-72{margin-bottom:-288px}.sl--ml-72{margin-left:-288px}.sl--mt-80{margin-top:-320px}.sl--mr-80{margin-right:-320px}.sl--mb-80{margin-bottom:-320px}.sl--ml-80{margin-left:-320px}.sl--mt-96{margin-top:-384px}.sl--mr-96{margin-right:-384px}.sl--mb-96{margin-bottom:-384px}.sl--ml-96{margin-left:-384px}.sl--mt-px{margin-top:-1px}.sl--mr-px{margin-right:-1px}.sl--mb-px{margin-bottom:-1px}.sl--ml-px{margin-left:-1px}.sl--mt-0\.5{margin-top:-2px}.sl--mr-0\.5{margin-right:-2px}.sl--mb-0\.5{margin-bottom:-2px}.sl--ml-0\.5{margin-left:-2px}.sl--mt-1\.5{margin-top:-6px}.sl--mr-1\.5{margin-right:-6px}.sl--mb-1\.5{margin-bottom:-6px}.sl--ml-1\.5{margin-left:-6px}.sl--mt-2\.5{margin-top:-10px}.sl--mr-2\.5{margin-right:-10px}.sl--mb-2\.5{margin-bottom:-10px}.sl--ml-2\.5{margin-left:-10px}.sl--mt-3\.5{margin-top:-14px}.sl--mr-3\.5{margin-right:-14px}.sl--mb-3\.5{margin-bottom:-14px}.sl--ml-3\.5{margin-left:-14px}.sl--mt-4\.5{margin-top:-18px}.sl--mr-4\.5{margin-right:-18px}.sl--mb-4\.5{margin-bottom:-18px}.sl--ml-4\.5{margin-left:-18px}.sl-max-h-full{max-height:100%}.sl-max-h-screen{max-height:100vh}.sl-max-w-none{max-width:none}.sl-max-w-full{max-width:100%}.sl-max-w-min{max-width:-moz-min-content;max-width:min-content}.sl-max-w-max{max-width:-moz-max-content;max-width:max-content}.sl-max-w-prose{max-width:65ch}.sl-min-h-full{min-height:100%}.sl-min-h-screen{min-height:100vh}.sl-min-w-full{min-width:100%}.sl-min-w-min{min-width:-moz-min-content;min-width:min-content}.sl-min-w-max{min-width:-moz-max-content;min-width:max-content}.sl-object-contain{object-fit:contain}.sl-object-cover{object-fit:cover}.sl-object-fill{object-fit:fill}.sl-object-none{object-fit:none}.sl-object-scale-down{object-fit:scale-down}.sl-object-bottom{object-position:bottom}.sl-object-center{object-position:center}.sl-object-left{object-position:left}.sl-object-left-bottom{object-position:left bottom}.sl-object-left-top{object-position:left top}.sl-object-right{object-position:right}.sl-object-right-bottom{object-position:right bottom}.sl-object-right-top{object-position:right top}.sl-object-top{object-position:top}.sl-opacity-0{opacity:0}.sl-opacity-5{opacity:.05}.sl-opacity-10{opacity:.1}.sl-opacity-20{opacity:.2}.sl-opacity-30{opacity:.3}.sl-opacity-40{opacity:.4}.sl-opacity-50{opacity:.5}.sl-opacity-60{opacity:.6}.sl-opacity-70{opacity:.7}.sl-opacity-90{opacity:.9}.sl-opacity-100{opacity:1}.hover\:sl-opacity-0:hover{opacity:0}.hover\:sl-opacity-5:hover{opacity:.05}.hover\:sl-opacity-10:hover{opacity:.1}.hover\:sl-opacity-20:hover{opacity:.2}.hover\:sl-opacity-30:hover{opacity:.3}.hover\:sl-opacity-40:hover{opacity:.4}.hover\:sl-opacity-50:hover{opacity:.5}.hover\:sl-opacity-60:hover{opacity:.6}.hover\:sl-opacity-70:hover{opacity:.7}.hover\:sl-opacity-90:hover{opacity:.9}.hover\:sl-opacity-100:hover{opacity:1}.focus\:sl-opacity-0:focus{opacity:0}.focus\:sl-opacity-5:focus{opacity:.05}.focus\:sl-opacity-10:focus{opacity:.1}.focus\:sl-opacity-20:focus{opacity:.2}.focus\:sl-opacity-30:focus{opacity:.3}.focus\:sl-opacity-40:focus{opacity:.4}.focus\:sl-opacity-50:focus{opacity:.5}.focus\:sl-opacity-60:focus{opacity:.6}.focus\:sl-opacity-70:focus{opacity:.7}.focus\:sl-opacity-90:focus{opacity:.9}.focus\:sl-opacity-100:focus{opacity:1}.active\:sl-opacity-0:active{opacity:0}.active\:sl-opacity-5:active{opacity:.05}.active\:sl-opacity-10:active{opacity:.1}.active\:sl-opacity-20:active{opacity:.2}.active\:sl-opacity-30:active{opacity:.3}.active\:sl-opacity-40:active{opacity:.4}.active\:sl-opacity-50:active{opacity:.5}.active\:sl-opacity-60:active{opacity:.6}.active\:sl-opacity-70:active{opacity:.7}.active\:sl-opacity-90:active{opacity:.9}.active\:sl-opacity-100:active{opacity:1}.disabled\:sl-opacity-0:disabled{opacity:0}.disabled\:sl-opacity-5:disabled{opacity:.05}.disabled\:sl-opacity-10:disabled{opacity:.1}.disabled\:sl-opacity-20:disabled{opacity:.2}.disabled\:sl-opacity-30:disabled{opacity:.3}.disabled\:sl-opacity-40:disabled{opacity:.4}.disabled\:sl-opacity-50:disabled{opacity:.5}.disabled\:sl-opacity-60:disabled{opacity:.6}.disabled\:sl-opacity-70:disabled{opacity:.7}.disabled\:sl-opacity-90:disabled{opacity:.9}.disabled\:sl-opacity-100:disabled{opacity:1}.sl-outline-none{outline:2px solid transparent;outline-offset:2px}.sl-overflow-auto{overflow:auto}.sl-overflow-hidden{overflow:hidden}.sl-overflow-visible{overflow:visible}.sl-overflow-scroll{overflow:scroll}.sl-overflow-x-auto{overflow-x:auto}.sl-overflow-y-auto{overflow-y:auto}.sl-overflow-x-hidden{overflow-x:hidden}.sl-overflow-y-hidden{overflow-y:hidden}.sl-overflow-x-visible{overflow-x:visible}.sl-overflow-y-visible{overflow-y:visible}.sl-overflow-x-scroll{overflow-x:scroll}.sl-overflow-y-scroll{overflow-y:scroll}.sl-overscroll-auto{overscroll-behavior:auto}.sl-overscroll-contain{overscroll-behavior:contain}.sl-overscroll-none{overscroll-behavior:none}.sl-overscroll-y-auto{overscroll-behavior-y:auto}.sl-overscroll-y-contain{overscroll-behavior-y:contain}.sl-overscroll-y-none{overscroll-behavior-y:none}.sl-overscroll-x-auto{overscroll-behavior-x:auto}.sl-overscroll-x-contain{overscroll-behavior-x:contain}.sl-overscroll-x-none{overscroll-behavior-x:none}.sl-p-0{padding:0}.sl-p-1{padding:4px}.sl-p-2{padding:8px}.sl-p-3{padding:12px}.sl-p-4{padding:16px}.sl-p-5{padding:20px}.sl-p-6{padding:24px}.sl-p-7{padding:28px}.sl-p-8{padding:32px}.sl-p-9{padding:36px}.sl-p-10{padding:40px}.sl-p-11{padding:44px}.sl-p-12{padding:48px}.sl-p-14{padding:56px}.sl-p-16{padding:64px}.sl-p-20{padding:80px}.sl-p-24{padding:96px}.sl-p-28{padding:112px}.sl-p-32{padding:128px}.sl-p-36{padding:144px}.sl-p-40{padding:160px}.sl-p-44{padding:176px}.sl-p-48{padding:192px}.sl-p-52{padding:208px}.sl-p-56{padding:224px}.sl-p-60{padding:240px}.sl-p-64{padding:256px}.sl-p-72{padding:288px}.sl-p-80{padding:320px}.sl-p-96{padding:384px}.sl-p-px{padding:1px}.sl-p-0\.5{padding:2px}.sl-p-1\.5{padding:6px}.sl-p-2\.5{padding:10px}.sl-p-3\.5{padding:14px}.sl-p-4\.5{padding:18px}.sl-py-0{padding-bottom:0;padding-top:0}.sl-px-0{padding-left:0;padding-right:0}.sl-py-1{padding-bottom:4px;padding-top:4px}.sl-px-1{padding-left:4px;padding-right:4px}.sl-py-2{padding-bottom:8px;padding-top:8px}.sl-px-2{padding-left:8px;padding-right:8px}.sl-py-3{padding-bottom:12px;padding-top:12px}.sl-px-3{padding-left:12px;padding-right:12px}.sl-py-4{padding-bottom:16px;padding-top:16px}.sl-px-4{padding-left:16px;padding-right:16px}.sl-py-5{padding-bottom:20px;padding-top:20px}.sl-px-5{padding-left:20px;padding-right:20px}.sl-py-6{padding-bottom:24px;padding-top:24px}.sl-px-6{padding-left:24px;padding-right:24px}.sl-py-7{padding-bottom:28px;padding-top:28px}.sl-px-7{padding-left:28px;padding-right:28px}.sl-py-8{padding-bottom:32px;padding-top:32px}.sl-px-8{padding-left:32px;padding-right:32px}.sl-py-9{padding-bottom:36px;padding-top:36px}.sl-px-9{padding-left:36px;padding-right:36px}.sl-py-10{padding-bottom:40px;padding-top:40px}.sl-px-10{padding-left:40px;padding-right:40px}.sl-py-11{padding-bottom:44px;padding-top:44px}.sl-px-11{padding-left:44px;padding-right:44px}.sl-py-12{padding-bottom:48px;padding-top:48px}.sl-px-12{padding-left:48px;padding-right:48px}.sl-py-14{padding-bottom:56px;padding-top:56px}.sl-px-14{padding-left:56px;padding-right:56px}.sl-py-16{padding-bottom:64px;padding-top:64px}.sl-px-16{padding-left:64px;padding-right:64px}.sl-py-20{padding-bottom:80px;padding-top:80px}.sl-px-20{padding-left:80px;padding-right:80px}.sl-py-24{padding-bottom:96px;padding-top:96px}.sl-px-24{padding-left:96px;padding-right:96px}.sl-py-28{padding-bottom:112px;padding-top:112px}.sl-px-28{padding-left:112px;padding-right:112px}.sl-py-32{padding-bottom:128px;padding-top:128px}.sl-px-32{padding-left:128px;padding-right:128px}.sl-py-36{padding-bottom:144px;padding-top:144px}.sl-px-36{padding-left:144px;padding-right:144px}.sl-py-40{padding-bottom:160px;padding-top:160px}.sl-px-40{padding-left:160px;padding-right:160px}.sl-py-44{padding-bottom:176px;padding-top:176px}.sl-px-44{padding-left:176px;padding-right:176px}.sl-py-48{padding-bottom:192px;padding-top:192px}.sl-px-48{padding-left:192px;padding-right:192px}.sl-py-52{padding-bottom:208px;padding-top:208px}.sl-px-52{padding-left:208px;padding-right:208px}.sl-py-56{padding-bottom:224px;padding-top:224px}.sl-px-56{padding-left:224px;padding-right:224px}.sl-py-60{padding-bottom:240px;padding-top:240px}.sl-px-60{padding-left:240px;padding-right:240px}.sl-py-64{padding-bottom:256px;padding-top:256px}.sl-px-64{padding-left:256px;padding-right:256px}.sl-py-72{padding-bottom:288px;padding-top:288px}.sl-px-72{padding-left:288px;padding-right:288px}.sl-py-80{padding-bottom:320px;padding-top:320px}.sl-px-80{padding-left:320px;padding-right:320px}.sl-py-96{padding-bottom:384px;padding-top:384px}.sl-px-96{padding-left:384px;padding-right:384px}.sl-py-px{padding-bottom:1px;padding-top:1px}.sl-px-px{padding-left:1px;padding-right:1px}.sl-py-0\.5{padding-bottom:2px;padding-top:2px}.sl-px-0\.5{padding-left:2px;padding-right:2px}.sl-py-1\.5{padding-bottom:6px;padding-top:6px}.sl-px-1\.5{padding-left:6px;padding-right:6px}.sl-py-2\.5{padding-bottom:10px;padding-top:10px}.sl-px-2\.5{padding-left:10px;padding-right:10px}.sl-py-3\.5{padding-bottom:14px;padding-top:14px}.sl-px-3\.5{padding-left:14px;padding-right:14px}.sl-py-4\.5{padding-bottom:18px;padding-top:18px}.sl-px-4\.5{padding-left:18px;padding-right:18px}.sl-pt-0{padding-top:0}.sl-pr-0{padding-right:0}.sl-pb-0{padding-bottom:0}.sl-pl-0{padding-left:0}.sl-pt-1{padding-top:4px}.sl-pr-1{padding-right:4px}.sl-pb-1{padding-bottom:4px}.sl-pl-1{padding-left:4px}.sl-pt-2{padding-top:8px}.sl-pr-2{padding-right:8px}.sl-pb-2{padding-bottom:8px}.sl-pl-2{padding-left:8px}.sl-pt-3{padding-top:12px}.sl-pr-3{padding-right:12px}.sl-pb-3{padding-bottom:12px}.sl-pl-3{padding-left:12px}.sl-pt-4{padding-top:16px}.sl-pr-4{padding-right:16px}.sl-pb-4{padding-bottom:16px}.sl-pl-4{padding-left:16px}.sl-pt-5{padding-top:20px}.sl-pr-5{padding-right:20px}.sl-pb-5{padding-bottom:20px}.sl-pl-5{padding-left:20px}.sl-pt-6{padding-top:24px}.sl-pr-6{padding-right:24px}.sl-pb-6{padding-bottom:24px}.sl-pl-6{padding-left:24px}.sl-pt-7{padding-top:28px}.sl-pr-7{padding-right:28px}.sl-pb-7{padding-bottom:28px}.sl-pl-7{padding-left:28px}.sl-pt-8{padding-top:32px}.sl-pr-8{padding-right:32px}.sl-pb-8{padding-bottom:32px}.sl-pl-8{padding-left:32px}.sl-pt-9{padding-top:36px}.sl-pr-9{padding-right:36px}.sl-pb-9{padding-bottom:36px}.sl-pl-9{padding-left:36px}.sl-pt-10{padding-top:40px}.sl-pr-10{padding-right:40px}.sl-pb-10{padding-bottom:40px}.sl-pl-10{padding-left:40px}.sl-pt-11{padding-top:44px}.sl-pr-11{padding-right:44px}.sl-pb-11{padding-bottom:44px}.sl-pl-11{padding-left:44px}.sl-pt-12{padding-top:48px}.sl-pr-12{padding-right:48px}.sl-pb-12{padding-bottom:48px}.sl-pl-12{padding-left:48px}.sl-pt-14{padding-top:56px}.sl-pr-14{padding-right:56px}.sl-pb-14{padding-bottom:56px}.sl-pl-14{padding-left:56px}.sl-pt-16{padding-top:64px}.sl-pr-16{padding-right:64px}.sl-pb-16{padding-bottom:64px}.sl-pl-16{padding-left:64px}.sl-pt-20{padding-top:80px}.sl-pr-20{padding-right:80px}.sl-pb-20{padding-bottom:80px}.sl-pl-20{padding-left:80px}.sl-pt-24{padding-top:96px}.sl-pr-24{padding-right:96px}.sl-pb-24{padding-bottom:96px}.sl-pl-24{padding-left:96px}.sl-pt-28{padding-top:112px}.sl-pr-28{padding-right:112px}.sl-pb-28{padding-bottom:112px}.sl-pl-28{padding-left:112px}.sl-pt-32{padding-top:128px}.sl-pr-32{padding-right:128px}.sl-pb-32{padding-bottom:128px}.sl-pl-32{padding-left:128px}.sl-pt-36{padding-top:144px}.sl-pr-36{padding-right:144px}.sl-pb-36{padding-bottom:144px}.sl-pl-36{padding-left:144px}.sl-pt-40{padding-top:160px}.sl-pr-40{padding-right:160px}.sl-pb-40{padding-bottom:160px}.sl-pl-40{padding-left:160px}.sl-pt-44{padding-top:176px}.sl-pr-44{padding-right:176px}.sl-pb-44{padding-bottom:176px}.sl-pl-44{padding-left:176px}.sl-pt-48{padding-top:192px}.sl-pr-48{padding-right:192px}.sl-pb-48{padding-bottom:192px}.sl-pl-48{padding-left:192px}.sl-pt-52{padding-top:208px}.sl-pr-52{padding-right:208px}.sl-pb-52{padding-bottom:208px}.sl-pl-52{padding-left:208px}.sl-pt-56{padding-top:224px}.sl-pr-56{padding-right:224px}.sl-pb-56{padding-bottom:224px}.sl-pl-56{padding-left:224px}.sl-pt-60{padding-top:240px}.sl-pr-60{padding-right:240px}.sl-pb-60{padding-bottom:240px}.sl-pl-60{padding-left:240px}.sl-pt-64{padding-top:256px}.sl-pr-64{padding-right:256px}.sl-pb-64{padding-bottom:256px}.sl-pl-64{padding-left:256px}.sl-pt-72{padding-top:288px}.sl-pr-72{padding-right:288px}.sl-pb-72{padding-bottom:288px}.sl-pl-72{padding-left:288px}.sl-pt-80{padding-top:320px}.sl-pr-80{padding-right:320px}.sl-pb-80{padding-bottom:320px}.sl-pl-80{padding-left:320px}.sl-pt-96{padding-top:384px}.sl-pr-96{padding-right:384px}.sl-pb-96{padding-bottom:384px}.sl-pl-96{padding-left:384px}.sl-pt-px{padding-top:1px}.sl-pr-px{padding-right:1px}.sl-pb-px{padding-bottom:1px}.sl-pl-px{padding-left:1px}.sl-pt-0\.5{padding-top:2px}.sl-pr-0\.5{padding-right:2px}.sl-pb-0\.5{padding-bottom:2px}.sl-pl-0\.5{padding-left:2px}.sl-pt-1\.5{padding-top:6px}.sl-pr-1\.5{padding-right:6px}.sl-pb-1\.5{padding-bottom:6px}.sl-pl-1\.5{padding-left:6px}.sl-pt-2\.5{padding-top:10px}.sl-pr-2\.5{padding-right:10px}.sl-pb-2\.5{padding-bottom:10px}.sl-pl-2\.5{padding-left:10px}.sl-pt-3\.5{padding-top:14px}.sl-pr-3\.5{padding-right:14px}.sl-pb-3\.5{padding-bottom:14px}.sl-pl-3\.5{padding-left:14px}.sl-pt-4\.5{padding-top:18px}.sl-pr-4\.5{padding-right:18px}.sl-pb-4\.5{padding-bottom:18px}.sl-pl-4\.5{padding-left:18px}.sl-placeholder::-ms-input-placeholder{color:var(--color-text-light)}.sl-placeholder::placeholder{color:var(--color-text-light)}.sl-placeholder-primary::-ms-input-placeholder{color:#3898ff}.sl-placeholder-primary::placeholder{color:#3898ff}.sl-placeholder-success::-ms-input-placeholder{color:#0ea06f}.sl-placeholder-success::placeholder{color:#0ea06f}.sl-placeholder-warning::-ms-input-placeholder{color:#f3602b}.sl-placeholder-warning::placeholder{color:#f3602b}.sl-placeholder-danger::-ms-input-placeholder{color:#f05151}.sl-placeholder-danger::placeholder{color:#f05151}.sl-pointer-events-none{pointer-events:none}.sl-pointer-events-auto{pointer-events:auto}.sl-static{position:static}.sl-fixed{position:fixed}.sl-absolute{position:absolute}.sl-relative{position:relative}.sl-sticky{position:-webkit-sticky;position:sticky}.sl-resize-none{resize:none}.sl-resize-y{resize:vertical}.sl-resize-x{resize:horizontal}.sl-resize{resize:both}.sl-ring-primary{--tw-ring-color:hsla(var(--primary-h),80%,61%,var(--tw-ring-opacity)) }.sl-ring-success{--tw-ring-color:hsla(var(--success-h),84%,34%,var(--tw-ring-opacity)) }.sl-ring-warning{--tw-ring-color:hsla(var(--warning-h),89%,56%,var(--tw-ring-opacity)) }.sl-ring-danger{--tw-ring-color:hsla(var(--danger-h),84%,63%,var(--tw-ring-opacity)) }.focus\:sl-ring-primary:focus{--tw-ring-color:hsla(var(--primary-h),80%,61%,var(--tw-ring-opacity)) }.focus\:sl-ring-success:focus{--tw-ring-color:hsla(var(--success-h),84%,34%,var(--tw-ring-opacity)) }.focus\:sl-ring-warning:focus{--tw-ring-color:hsla(var(--warning-h),89%,56%,var(--tw-ring-opacity)) }.focus\:sl-ring-danger:focus{--tw-ring-color:hsla(var(--danger-h),84%,63%,var(--tw-ring-opacity)) }.sl-ring-opacity-0{--tw-ring-opacity:0}.sl-ring-opacity-5{--tw-ring-opacity:0.05}.sl-ring-opacity-10{--tw-ring-opacity:0.1}.sl-ring-opacity-20{--tw-ring-opacity:0.2}.sl-ring-opacity-30{--tw-ring-opacity:0.3}.sl-ring-opacity-40{--tw-ring-opacity:0.4}.sl-ring-opacity-50{--tw-ring-opacity:0.5}.sl-ring-opacity-60{--tw-ring-opacity:0.6}.sl-ring-opacity-70{--tw-ring-opacity:0.7}.sl-ring-opacity-90{--tw-ring-opacity:0.9}.sl-ring-opacity-100{--tw-ring-opacity:1}.focus\:sl-ring-opacity-0:focus{--tw-ring-opacity:0}.focus\:sl-ring-opacity-5:focus{--tw-ring-opacity:0.05}.focus\:sl-ring-opacity-10:focus{--tw-ring-opacity:0.1}.focus\:sl-ring-opacity-20:focus{--tw-ring-opacity:0.2}.focus\:sl-ring-opacity-30:focus{--tw-ring-opacity:0.3}.focus\:sl-ring-opacity-40:focus{--tw-ring-opacity:0.4}.focus\:sl-ring-opacity-50:focus{--tw-ring-opacity:0.5}.focus\:sl-ring-opacity-60:focus{--tw-ring-opacity:0.6}.focus\:sl-ring-opacity-70:focus{--tw-ring-opacity:0.7}.focus\:sl-ring-opacity-90:focus{--tw-ring-opacity:0.9}.focus\:sl-ring-opacity-100:focus{--tw-ring-opacity:1}*{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(147,197,253,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000}.sl-ring{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.sl-ring-inset{--tw-ring-inset:inset}.focus\:sl-ring:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:sl-ring-inset:focus{--tw-ring-inset:inset}.sl-stroke-transparent{stroke:transparent}.sl-stroke-current{stroke:currentColor}.sl-stroke-lighten-100{stroke:var(--color-lighten-100)}.sl-stroke-darken-100{stroke:var(--color-darken-100)}.sl-stroke-primary{stroke:var(--color-primary)}.sl-stroke-primary-tint{stroke:var(--color-primary-tint)}.sl-stroke-primary-light{stroke:var(--color-primary-light)}.sl-stroke-primary-dark{stroke:var(--color-primary-dark)}.sl-stroke-primary-darker{stroke:var(--color-primary-darker)}.sl-stroke-success{stroke:var(--color-success)}.sl-stroke-success-tint{stroke:var(--color-success-tint)}.sl-stroke-success-light{stroke:var(--color-success-light)}.sl-stroke-success-dark{stroke:var(--color-success-dark)}.sl-stroke-success-darker{stroke:var(--color-success-darker)}.sl-stroke-warning{stroke:var(--color-warning)}.sl-stroke-warning-tint{stroke:var(--color-warning-tint)}.sl-stroke-warning-light{stroke:var(--color-warning-light)}.sl-stroke-warning-dark{stroke:var(--color-warning-dark)}.sl-stroke-warning-darker{stroke:var(--color-warning-darker)}.sl-stroke-danger{stroke:var(--color-danger)}.sl-stroke-danger-tint{stroke:var(--color-danger-tint)}.sl-stroke-danger-light{stroke:var(--color-danger-light)}.sl-stroke-danger-dark{stroke:var(--color-danger-dark)}.sl-stroke-danger-darker{stroke:var(--color-danger-darker)}.sl-stroke-code{stroke:var(--color-code)}.sl-stroke-on-code{stroke:var(--color-on-code)}.sl-stroke-on-primary{stroke:var(--color-on-primary)}.sl-stroke-on-success{stroke:var(--color-on-success)}.sl-stroke-on-warning{stroke:var(--color-on-warning)}.sl-stroke-on-danger{stroke:var(--color-on-danger)}.sl-stroke-text{stroke:var(--color-text)}.sl-table-auto{table-layout:auto}.sl-table-fixed{table-layout:fixed}.sl-text-left{text-align:left}.sl-text-center{text-align:center}.sl-text-right{text-align:right}.sl-text-justify{text-align:justify}.sl-text-transparent{color:transparent}.sl-text-current{color:currentColor}.sl-text-lighten-100{color:var(--color-lighten-100)}.sl-text-darken-100{color:var(--color-darken-100)}.sl-text-primary{color:var(--color-primary)}.sl-text-primary-tint{color:var(--color-primary-tint)}.sl-text-primary-light{color:var(--color-primary-light)}.sl-text-primary-dark{color:var(--color-primary-dark)}.sl-text-primary-darker{color:var(--color-primary-darker)}.sl-text-success{color:var(--color-success)}.sl-text-success-tint{color:var(--color-success-tint)}.sl-text-success-light{color:var(--color-success-light)}.sl-text-success-dark{color:var(--color-success-dark)}.sl-text-success-darker{color:var(--color-success-darker)}.sl-text-warning{color:var(--color-warning)}.sl-text-warning-tint{color:var(--color-warning-tint)}.sl-text-warning-light{color:var(--color-warning-light)}.sl-text-warning-dark{color:var(--color-warning-dark)}.sl-text-warning-darker{color:var(--color-warning-darker)}.sl-text-danger{color:var(--color-danger)}.sl-text-danger-tint{color:var(--color-danger-tint)}.sl-text-danger-light{color:var(--color-danger-light)}.sl-text-danger-dark{color:var(--color-danger-dark)}.sl-text-danger-darker{color:var(--color-danger-darker)}.sl-text-code{color:var(--color-code)}.sl-text-on-code{color:var(--color-on-code)}.sl-text-on-primary{color:var(--color-on-primary)}.sl-text-on-success{color:var(--color-on-success)}.sl-text-on-warning{color:var(--color-on-warning)}.sl-text-on-danger{color:var(--color-on-danger)}.sl-text-body{color:var(--color-text)}.sl-text-muted{color:var(--color-text-muted)}.sl-text-light{color:var(--color-text-light)}.sl-text-heading{color:var(--color-text-heading)}.sl-text-paragraph{color:var(--color-text-paragraph)}.sl-text-canvas-50{color:var(--color-canvas-50)}.sl-text-canvas-100{color:var(--color-canvas-100)}.sl-text-canvas-200{color:var(--color-canvas-200)}.sl-text-canvas-300{color:var(--color-canvas-300)}.sl-text-canvas-pure{color:var(--color-canvas-pure)}.sl-text-canvas{color:var(--color-canvas)}.sl-text-canvas-dialog{color:var(--color-canvas-dialog)}.sl-text-link{color:var(--color-link)}.sl-text-link-dark{color:var(--color-link-dark)}.hover\:sl-text-transparent:hover{color:transparent}.hover\:sl-text-current:hover{color:currentColor}.hover\:sl-text-lighten-100:hover{color:var(--color-lighten-100)}.hover\:sl-text-darken-100:hover{color:var(--color-darken-100)}.hover\:sl-text-primary:hover{color:var(--color-primary)}.hover\:sl-text-primary-tint:hover{color:var(--color-primary-tint)}.hover\:sl-text-primary-light:hover{color:var(--color-primary-light)}.hover\:sl-text-primary-dark:hover{color:var(--color-primary-dark)}.hover\:sl-text-primary-darker:hover{color:var(--color-primary-darker)}.hover\:sl-text-success:hover{color:var(--color-success)}.hover\:sl-text-success-tint:hover{color:var(--color-success-tint)}.hover\:sl-text-success-light:hover{color:var(--color-success-light)}.hover\:sl-text-success-dark:hover{color:var(--color-success-dark)}.hover\:sl-text-success-darker:hover{color:var(--color-success-darker)}.hover\:sl-text-warning:hover{color:var(--color-warning)}.hover\:sl-text-warning-tint:hover{color:var(--color-warning-tint)}.hover\:sl-text-warning-light:hover{color:var(--color-warning-light)}.hover\:sl-text-warning-dark:hover{color:var(--color-warning-dark)}.hover\:sl-text-warning-darker:hover{color:var(--color-warning-darker)}.hover\:sl-text-danger:hover{color:var(--color-danger)}.hover\:sl-text-danger-tint:hover{color:var(--color-danger-tint)}.hover\:sl-text-danger-light:hover{color:var(--color-danger-light)}.hover\:sl-text-danger-dark:hover{color:var(--color-danger-dark)}.hover\:sl-text-danger-darker:hover{color:var(--color-danger-darker)}.hover\:sl-text-code:hover{color:var(--color-code)}.hover\:sl-text-on-code:hover{color:var(--color-on-code)}.hover\:sl-text-on-primary:hover{color:var(--color-on-primary)}.hover\:sl-text-on-success:hover{color:var(--color-on-success)}.hover\:sl-text-on-warning:hover{color:var(--color-on-warning)}.hover\:sl-text-on-danger:hover{color:var(--color-on-danger)}.hover\:sl-text-body:hover{color:var(--color-text)}.hover\:sl-text-muted:hover{color:var(--color-text-muted)}.hover\:sl-text-light:hover{color:var(--color-text-light)}.hover\:sl-text-heading:hover{color:var(--color-text-heading)}.hover\:sl-text-paragraph:hover{color:var(--color-text-paragraph)}.hover\:sl-text-canvas-50:hover{color:var(--color-canvas-50)}.hover\:sl-text-canvas-100:hover{color:var(--color-canvas-100)}.hover\:sl-text-canvas-200:hover{color:var(--color-canvas-200)}.hover\:sl-text-canvas-300:hover{color:var(--color-canvas-300)}.hover\:sl-text-canvas-pure:hover{color:var(--color-canvas-pure)}.hover\:sl-text-canvas:hover{color:var(--color-canvas)}.hover\:sl-text-canvas-dialog:hover{color:var(--color-canvas-dialog)}.hover\:sl-text-link:hover{color:var(--color-link)}.hover\:sl-text-link-dark:hover{color:var(--color-link-dark)}.focus\:sl-text-transparent:focus{color:transparent}.focus\:sl-text-current:focus{color:currentColor}.focus\:sl-text-lighten-100:focus{color:var(--color-lighten-100)}.focus\:sl-text-darken-100:focus{color:var(--color-darken-100)}.focus\:sl-text-primary:focus{color:var(--color-primary)}.focus\:sl-text-primary-tint:focus{color:var(--color-primary-tint)}.focus\:sl-text-primary-light:focus{color:var(--color-primary-light)}.focus\:sl-text-primary-dark:focus{color:var(--color-primary-dark)}.focus\:sl-text-primary-darker:focus{color:var(--color-primary-darker)}.focus\:sl-text-success:focus{color:var(--color-success)}.focus\:sl-text-success-tint:focus{color:var(--color-success-tint)}.focus\:sl-text-success-light:focus{color:var(--color-success-light)}.focus\:sl-text-success-dark:focus{color:var(--color-success-dark)}.focus\:sl-text-success-darker:focus{color:var(--color-success-darker)}.focus\:sl-text-warning:focus{color:var(--color-warning)}.focus\:sl-text-warning-tint:focus{color:var(--color-warning-tint)}.focus\:sl-text-warning-light:focus{color:var(--color-warning-light)}.focus\:sl-text-warning-dark:focus{color:var(--color-warning-dark)}.focus\:sl-text-warning-darker:focus{color:var(--color-warning-darker)}.focus\:sl-text-danger:focus{color:var(--color-danger)}.focus\:sl-text-danger-tint:focus{color:var(--color-danger-tint)}.focus\:sl-text-danger-light:focus{color:var(--color-danger-light)}.focus\:sl-text-danger-dark:focus{color:var(--color-danger-dark)}.focus\:sl-text-danger-darker:focus{color:var(--color-danger-darker)}.focus\:sl-text-code:focus{color:var(--color-code)}.focus\:sl-text-on-code:focus{color:var(--color-on-code)}.focus\:sl-text-on-primary:focus{color:var(--color-on-primary)}.focus\:sl-text-on-success:focus{color:var(--color-on-success)}.focus\:sl-text-on-warning:focus{color:var(--color-on-warning)}.focus\:sl-text-on-danger:focus{color:var(--color-on-danger)}.focus\:sl-text-body:focus{color:var(--color-text)}.focus\:sl-text-muted:focus{color:var(--color-text-muted)}.focus\:sl-text-light:focus{color:var(--color-text-light)}.focus\:sl-text-heading:focus{color:var(--color-text-heading)}.focus\:sl-text-paragraph:focus{color:var(--color-text-paragraph)}.focus\:sl-text-canvas-50:focus{color:var(--color-canvas-50)}.focus\:sl-text-canvas-100:focus{color:var(--color-canvas-100)}.focus\:sl-text-canvas-200:focus{color:var(--color-canvas-200)}.focus\:sl-text-canvas-300:focus{color:var(--color-canvas-300)}.focus\:sl-text-canvas-pure:focus{color:var(--color-canvas-pure)}.focus\:sl-text-canvas:focus{color:var(--color-canvas)}.focus\:sl-text-canvas-dialog:focus{color:var(--color-canvas-dialog)}.focus\:sl-text-link:focus{color:var(--color-link)}.focus\:sl-text-link-dark:focus{color:var(--color-link-dark)}.disabled\:sl-text-transparent:disabled{color:transparent}.disabled\:sl-text-current:disabled{color:currentColor}.disabled\:sl-text-lighten-100:disabled{color:var(--color-lighten-100)}.disabled\:sl-text-darken-100:disabled{color:var(--color-darken-100)}.disabled\:sl-text-primary:disabled{color:var(--color-primary)}.disabled\:sl-text-primary-tint:disabled{color:var(--color-primary-tint)}.disabled\:sl-text-primary-light:disabled{color:var(--color-primary-light)}.disabled\:sl-text-primary-dark:disabled{color:var(--color-primary-dark)}.disabled\:sl-text-primary-darker:disabled{color:var(--color-primary-darker)}.disabled\:sl-text-success:disabled{color:var(--color-success)}.disabled\:sl-text-success-tint:disabled{color:var(--color-success-tint)}.disabled\:sl-text-success-light:disabled{color:var(--color-success-light)}.disabled\:sl-text-success-dark:disabled{color:var(--color-success-dark)}.disabled\:sl-text-success-darker:disabled{color:var(--color-success-darker)}.disabled\:sl-text-warning:disabled{color:var(--color-warning)}.disabled\:sl-text-warning-tint:disabled{color:var(--color-warning-tint)}.disabled\:sl-text-warning-light:disabled{color:var(--color-warning-light)}.disabled\:sl-text-warning-dark:disabled{color:var(--color-warning-dark)}.disabled\:sl-text-warning-darker:disabled{color:var(--color-warning-darker)}.disabled\:sl-text-danger:disabled{color:var(--color-danger)}.disabled\:sl-text-danger-tint:disabled{color:var(--color-danger-tint)}.disabled\:sl-text-danger-light:disabled{color:var(--color-danger-light)}.disabled\:sl-text-danger-dark:disabled{color:var(--color-danger-dark)}.disabled\:sl-text-danger-darker:disabled{color:var(--color-danger-darker)}.disabled\:sl-text-code:disabled{color:var(--color-code)}.disabled\:sl-text-on-code:disabled{color:var(--color-on-code)}.disabled\:sl-text-on-primary:disabled{color:var(--color-on-primary)}.disabled\:sl-text-on-success:disabled{color:var(--color-on-success)}.disabled\:sl-text-on-warning:disabled{color:var(--color-on-warning)}.disabled\:sl-text-on-danger:disabled{color:var(--color-on-danger)}.disabled\:sl-text-body:disabled{color:var(--color-text)}.disabled\:sl-text-muted:disabled{color:var(--color-text-muted)}.disabled\:sl-text-light:disabled{color:var(--color-text-light)}.disabled\:sl-text-heading:disabled{color:var(--color-text-heading)}.disabled\:sl-text-paragraph:disabled{color:var(--color-text-paragraph)}.disabled\:sl-text-canvas-50:disabled{color:var(--color-canvas-50)}.disabled\:sl-text-canvas-100:disabled{color:var(--color-canvas-100)}.disabled\:sl-text-canvas-200:disabled{color:var(--color-canvas-200)}.disabled\:sl-text-canvas-300:disabled{color:var(--color-canvas-300)}.disabled\:sl-text-canvas-pure:disabled{color:var(--color-canvas-pure)}.disabled\:sl-text-canvas:disabled{color:var(--color-canvas)}.disabled\:sl-text-canvas-dialog:disabled{color:var(--color-canvas-dialog)}.disabled\:sl-text-link:disabled{color:var(--color-link)}.disabled\:sl-text-link-dark:disabled{color:var(--color-link-dark)}.sl-underline{text-decoration:underline}.sl-line-through{text-decoration:line-through}.sl-no-underline{text-decoration:none}.hover\:sl-underline:hover{text-decoration:underline}.hover\:sl-line-through:hover{text-decoration:line-through}.hover\:sl-no-underline:hover{text-decoration:none}.sl-truncate{overflow:hidden;white-space:nowrap}.sl-overflow-ellipsis,.sl-truncate{text-overflow:ellipsis}.sl-overflow-clip{text-overflow:clip}.sl-uppercase{text-transform:uppercase}.sl-lowercase{text-transform:lowercase}.sl-capitalize{text-transform:capitalize}.sl-normal-case{text-transform:none}.sl-transform{transform:translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sl-transform,.sl-transform-gpu{--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1}.sl-transform-gpu{transform:translate3d(var(--tw-translate-x),var(--tw-translate-y),0) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sl-transform-none{transform:none}.sl-delay-75{transition-delay:75ms}.sl-delay-150{transition-delay:.15s}.sl-delay-300{transition-delay:.3s}.sl-delay-500{transition-delay:.5s}.sl-delay-1000{transition-delay:1s}.sl-duration-75{transition-duration:75ms}.sl-duration-150{transition-duration:.15s}.sl-duration-300{transition-duration:.3s}.sl-duration-500{transition-duration:.5s}.sl-duration-1000{transition-duration:1s}.sl-transition{transition-duration:.15s;transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.sl-translate-x-0{--tw-translate-x:0px}.sl-translate-x-1{--tw-translate-x:4px}.sl-translate-x-2{--tw-translate-x:8px}.sl-translate-x-3{--tw-translate-x:12px}.sl-translate-x-4{--tw-translate-x:16px}.sl-translate-x-5{--tw-translate-x:20px}.sl-translate-x-6{--tw-translate-x:24px}.sl-translate-x-7{--tw-translate-x:28px}.sl-translate-x-8{--tw-translate-x:32px}.sl-translate-x-9{--tw-translate-x:36px}.sl-translate-x-10{--tw-translate-x:40px}.sl-translate-x-11{--tw-translate-x:44px}.sl-translate-x-12{--tw-translate-x:48px}.sl-translate-x-14{--tw-translate-x:56px}.sl-translate-x-16{--tw-translate-x:64px}.sl-translate-x-20{--tw-translate-x:80px}.sl-translate-x-24{--tw-translate-x:96px}.sl-translate-x-28{--tw-translate-x:112px}.sl-translate-x-32{--tw-translate-x:128px}.sl-translate-x-36{--tw-translate-x:144px}.sl-translate-x-40{--tw-translate-x:160px}.sl-translate-x-44{--tw-translate-x:176px}.sl-translate-x-48{--tw-translate-x:192px}.sl-translate-x-52{--tw-translate-x:208px}.sl-translate-x-56{--tw-translate-x:224px}.sl-translate-x-60{--tw-translate-x:240px}.sl-translate-x-64{--tw-translate-x:256px}.sl-translate-x-72{--tw-translate-x:288px}.sl-translate-x-80{--tw-translate-x:320px}.sl-translate-x-96{--tw-translate-x:384px}.sl-translate-x-px{--tw-translate-x:1px}.sl-translate-x-0\.5{--tw-translate-x:2px}.sl-translate-x-1\.5{--tw-translate-x:6px}.sl-translate-x-2\.5{--tw-translate-x:10px}.sl-translate-x-3\.5{--tw-translate-x:14px}.sl-translate-x-4\.5{--tw-translate-x:18px}.sl--translate-x-0{--tw-translate-x:0px}.sl--translate-x-1{--tw-translate-x:-4px}.sl--translate-x-2{--tw-translate-x:-8px}.sl--translate-x-3{--tw-translate-x:-12px}.sl--translate-x-4{--tw-translate-x:-16px}.sl--translate-x-5{--tw-translate-x:-20px}.sl--translate-x-6{--tw-translate-x:-24px}.sl--translate-x-7{--tw-translate-x:-28px}.sl--translate-x-8{--tw-translate-x:-32px}.sl--translate-x-9{--tw-translate-x:-36px}.sl--translate-x-10{--tw-translate-x:-40px}.sl--translate-x-11{--tw-translate-x:-44px}.sl--translate-x-12{--tw-translate-x:-48px}.sl--translate-x-14{--tw-translate-x:-56px}.sl--translate-x-16{--tw-translate-x:-64px}.sl--translate-x-20{--tw-translate-x:-80px}.sl--translate-x-24{--tw-translate-x:-96px}.sl--translate-x-28{--tw-translate-x:-112px}.sl--translate-x-32{--tw-translate-x:-128px}.sl--translate-x-36{--tw-translate-x:-144px}.sl--translate-x-40{--tw-translate-x:-160px}.sl--translate-x-44{--tw-translate-x:-176px}.sl--translate-x-48{--tw-translate-x:-192px}.sl--translate-x-52{--tw-translate-x:-208px}.sl--translate-x-56{--tw-translate-x:-224px}.sl--translate-x-60{--tw-translate-x:-240px}.sl--translate-x-64{--tw-translate-x:-256px}.sl--translate-x-72{--tw-translate-x:-288px}.sl--translate-x-80{--tw-translate-x:-320px}.sl--translate-x-96{--tw-translate-x:-384px}.sl--translate-x-px{--tw-translate-x:-1px}.sl--translate-x-0\.5{--tw-translate-x:-2px}.sl--translate-x-1\.5{--tw-translate-x:-6px}.sl--translate-x-2\.5{--tw-translate-x:-10px}.sl--translate-x-3\.5{--tw-translate-x:-14px}.sl--translate-x-4\.5{--tw-translate-x:-18px}.sl-translate-y-0{--tw-translate-y:0px}.sl-translate-y-1{--tw-translate-y:4px}.sl-translate-y-2{--tw-translate-y:8px}.sl-translate-y-3{--tw-translate-y:12px}.sl-translate-y-4{--tw-translate-y:16px}.sl-translate-y-5{--tw-translate-y:20px}.sl-translate-y-6{--tw-translate-y:24px}.sl-translate-y-7{--tw-translate-y:28px}.sl-translate-y-8{--tw-translate-y:32px}.sl-translate-y-9{--tw-translate-y:36px}.sl-translate-y-10{--tw-translate-y:40px}.sl-translate-y-11{--tw-translate-y:44px}.sl-translate-y-12{--tw-translate-y:48px}.sl-translate-y-14{--tw-translate-y:56px}.sl-translate-y-16{--tw-translate-y:64px}.sl-translate-y-20{--tw-translate-y:80px}.sl-translate-y-24{--tw-translate-y:96px}.sl-translate-y-28{--tw-translate-y:112px}.sl-translate-y-32{--tw-translate-y:128px}.sl-translate-y-36{--tw-translate-y:144px}.sl-translate-y-40{--tw-translate-y:160px}.sl-translate-y-44{--tw-translate-y:176px}.sl-translate-y-48{--tw-translate-y:192px}.sl-translate-y-52{--tw-translate-y:208px}.sl-translate-y-56{--tw-translate-y:224px}.sl-translate-y-60{--tw-translate-y:240px}.sl-translate-y-64{--tw-translate-y:256px}.sl-translate-y-72{--tw-translate-y:288px}.sl-translate-y-80{--tw-translate-y:320px}.sl-translate-y-96{--tw-translate-y:384px}.sl-translate-y-px{--tw-translate-y:1px}.sl-translate-y-0\.5{--tw-translate-y:2px}.sl-translate-y-1\.5{--tw-translate-y:6px}.sl-translate-y-2\.5{--tw-translate-y:10px}.sl-translate-y-3\.5{--tw-translate-y:14px}.sl-translate-y-4\.5{--tw-translate-y:18px}.sl--translate-y-0{--tw-translate-y:0px}.sl--translate-y-1{--tw-translate-y:-4px}.sl--translate-y-2{--tw-translate-y:-8px}.sl--translate-y-3{--tw-translate-y:-12px}.sl--translate-y-4{--tw-translate-y:-16px}.sl--translate-y-5{--tw-translate-y:-20px}.sl--translate-y-6{--tw-translate-y:-24px}.sl--translate-y-7{--tw-translate-y:-28px}.sl--translate-y-8{--tw-translate-y:-32px}.sl--translate-y-9{--tw-translate-y:-36px}.sl--translate-y-10{--tw-translate-y:-40px}.sl--translate-y-11{--tw-translate-y:-44px}.sl--translate-y-12{--tw-translate-y:-48px}.sl--translate-y-14{--tw-translate-y:-56px}.sl--translate-y-16{--tw-translate-y:-64px}.sl--translate-y-20{--tw-translate-y:-80px}.sl--translate-y-24{--tw-translate-y:-96px}.sl--translate-y-28{--tw-translate-y:-112px}.sl--translate-y-32{--tw-translate-y:-128px}.sl--translate-y-36{--tw-translate-y:-144px}.sl--translate-y-40{--tw-translate-y:-160px}.sl--translate-y-44{--tw-translate-y:-176px}.sl--translate-y-48{--tw-translate-y:-192px}.sl--translate-y-52{--tw-translate-y:-208px}.sl--translate-y-56{--tw-translate-y:-224px}.sl--translate-y-60{--tw-translate-y:-240px}.sl--translate-y-64{--tw-translate-y:-256px}.sl--translate-y-72{--tw-translate-y:-288px}.sl--translate-y-80{--tw-translate-y:-320px}.sl--translate-y-96{--tw-translate-y:-384px}.sl--translate-y-px{--tw-translate-y:-1px}.sl--translate-y-0\.5{--tw-translate-y:-2px}.sl--translate-y-1\.5{--tw-translate-y:-6px}.sl--translate-y-2\.5{--tw-translate-y:-10px}.sl--translate-y-3\.5{--tw-translate-y:-14px}.sl--translate-y-4\.5{--tw-translate-y:-18px}.hover\:sl-translate-x-0:hover{--tw-translate-x:0px}.hover\:sl-translate-x-1:hover{--tw-translate-x:4px}.hover\:sl-translate-x-2:hover{--tw-translate-x:8px}.hover\:sl-translate-x-3:hover{--tw-translate-x:12px}.hover\:sl-translate-x-4:hover{--tw-translate-x:16px}.hover\:sl-translate-x-5:hover{--tw-translate-x:20px}.hover\:sl-translate-x-6:hover{--tw-translate-x:24px}.hover\:sl-translate-x-7:hover{--tw-translate-x:28px}.hover\:sl-translate-x-8:hover{--tw-translate-x:32px}.hover\:sl-translate-x-9:hover{--tw-translate-x:36px}.hover\:sl-translate-x-10:hover{--tw-translate-x:40px}.hover\:sl-translate-x-11:hover{--tw-translate-x:44px}.hover\:sl-translate-x-12:hover{--tw-translate-x:48px}.hover\:sl-translate-x-14:hover{--tw-translate-x:56px}.hover\:sl-translate-x-16:hover{--tw-translate-x:64px}.hover\:sl-translate-x-20:hover{--tw-translate-x:80px}.hover\:sl-translate-x-24:hover{--tw-translate-x:96px}.hover\:sl-translate-x-28:hover{--tw-translate-x:112px}.hover\:sl-translate-x-32:hover{--tw-translate-x:128px}.hover\:sl-translate-x-36:hover{--tw-translate-x:144px}.hover\:sl-translate-x-40:hover{--tw-translate-x:160px}.hover\:sl-translate-x-44:hover{--tw-translate-x:176px}.hover\:sl-translate-x-48:hover{--tw-translate-x:192px}.hover\:sl-translate-x-52:hover{--tw-translate-x:208px}.hover\:sl-translate-x-56:hover{--tw-translate-x:224px}.hover\:sl-translate-x-60:hover{--tw-translate-x:240px}.hover\:sl-translate-x-64:hover{--tw-translate-x:256px}.hover\:sl-translate-x-72:hover{--tw-translate-x:288px}.hover\:sl-translate-x-80:hover{--tw-translate-x:320px}.hover\:sl-translate-x-96:hover{--tw-translate-x:384px}.hover\:sl-translate-x-px:hover{--tw-translate-x:1px}.hover\:sl-translate-x-0\.5:hover{--tw-translate-x:2px}.hover\:sl-translate-x-1\.5:hover{--tw-translate-x:6px}.hover\:sl-translate-x-2\.5:hover{--tw-translate-x:10px}.hover\:sl-translate-x-3\.5:hover{--tw-translate-x:14px}.hover\:sl-translate-x-4\.5:hover{--tw-translate-x:18px}.hover\:sl--translate-x-0:hover{--tw-translate-x:0px}.hover\:sl--translate-x-1:hover{--tw-translate-x:-4px}.hover\:sl--translate-x-2:hover{--tw-translate-x:-8px}.hover\:sl--translate-x-3:hover{--tw-translate-x:-12px}.hover\:sl--translate-x-4:hover{--tw-translate-x:-16px}.hover\:sl--translate-x-5:hover{--tw-translate-x:-20px}.hover\:sl--translate-x-6:hover{--tw-translate-x:-24px}.hover\:sl--translate-x-7:hover{--tw-translate-x:-28px}.hover\:sl--translate-x-8:hover{--tw-translate-x:-32px}.hover\:sl--translate-x-9:hover{--tw-translate-x:-36px}.hover\:sl--translate-x-10:hover{--tw-translate-x:-40px}.hover\:sl--translate-x-11:hover{--tw-translate-x:-44px}.hover\:sl--translate-x-12:hover{--tw-translate-x:-48px}.hover\:sl--translate-x-14:hover{--tw-translate-x:-56px}.hover\:sl--translate-x-16:hover{--tw-translate-x:-64px}.hover\:sl--translate-x-20:hover{--tw-translate-x:-80px}.hover\:sl--translate-x-24:hover{--tw-translate-x:-96px}.hover\:sl--translate-x-28:hover{--tw-translate-x:-112px}.hover\:sl--translate-x-32:hover{--tw-translate-x:-128px}.hover\:sl--translate-x-36:hover{--tw-translate-x:-144px}.hover\:sl--translate-x-40:hover{--tw-translate-x:-160px}.hover\:sl--translate-x-44:hover{--tw-translate-x:-176px}.hover\:sl--translate-x-48:hover{--tw-translate-x:-192px}.hover\:sl--translate-x-52:hover{--tw-translate-x:-208px}.hover\:sl--translate-x-56:hover{--tw-translate-x:-224px}.hover\:sl--translate-x-60:hover{--tw-translate-x:-240px}.hover\:sl--translate-x-64:hover{--tw-translate-x:-256px}.hover\:sl--translate-x-72:hover{--tw-translate-x:-288px}.hover\:sl--translate-x-80:hover{--tw-translate-x:-320px}.hover\:sl--translate-x-96:hover{--tw-translate-x:-384px}.hover\:sl--translate-x-px:hover{--tw-translate-x:-1px}.hover\:sl--translate-x-0\.5:hover{--tw-translate-x:-2px}.hover\:sl--translate-x-1\.5:hover{--tw-translate-x:-6px}.hover\:sl--translate-x-2\.5:hover{--tw-translate-x:-10px}.hover\:sl--translate-x-3\.5:hover{--tw-translate-x:-14px}.hover\:sl--translate-x-4\.5:hover{--tw-translate-x:-18px}.hover\:sl-translate-y-0:hover{--tw-translate-y:0px}.hover\:sl-translate-y-1:hover{--tw-translate-y:4px}.hover\:sl-translate-y-2:hover{--tw-translate-y:8px}.hover\:sl-translate-y-3:hover{--tw-translate-y:12px}.hover\:sl-translate-y-4:hover{--tw-translate-y:16px}.hover\:sl-translate-y-5:hover{--tw-translate-y:20px}.hover\:sl-translate-y-6:hover{--tw-translate-y:24px}.hover\:sl-translate-y-7:hover{--tw-translate-y:28px}.hover\:sl-translate-y-8:hover{--tw-translate-y:32px}.hover\:sl-translate-y-9:hover{--tw-translate-y:36px}.hover\:sl-translate-y-10:hover{--tw-translate-y:40px}.hover\:sl-translate-y-11:hover{--tw-translate-y:44px}.hover\:sl-translate-y-12:hover{--tw-translate-y:48px}.hover\:sl-translate-y-14:hover{--tw-translate-y:56px}.hover\:sl-translate-y-16:hover{--tw-translate-y:64px}.hover\:sl-translate-y-20:hover{--tw-translate-y:80px}.hover\:sl-translate-y-24:hover{--tw-translate-y:96px}.hover\:sl-translate-y-28:hover{--tw-translate-y:112px}.hover\:sl-translate-y-32:hover{--tw-translate-y:128px}.hover\:sl-translate-y-36:hover{--tw-translate-y:144px}.hover\:sl-translate-y-40:hover{--tw-translate-y:160px}.hover\:sl-translate-y-44:hover{--tw-translate-y:176px}.hover\:sl-translate-y-48:hover{--tw-translate-y:192px}.hover\:sl-translate-y-52:hover{--tw-translate-y:208px}.hover\:sl-translate-y-56:hover{--tw-translate-y:224px}.hover\:sl-translate-y-60:hover{--tw-translate-y:240px}.hover\:sl-translate-y-64:hover{--tw-translate-y:256px}.hover\:sl-translate-y-72:hover{--tw-translate-y:288px}.hover\:sl-translate-y-80:hover{--tw-translate-y:320px}.hover\:sl-translate-y-96:hover{--tw-translate-y:384px}.hover\:sl-translate-y-px:hover{--tw-translate-y:1px}.hover\:sl-translate-y-0\.5:hover{--tw-translate-y:2px}.hover\:sl-translate-y-1\.5:hover{--tw-translate-y:6px}.hover\:sl-translate-y-2\.5:hover{--tw-translate-y:10px}.hover\:sl-translate-y-3\.5:hover{--tw-translate-y:14px}.hover\:sl-translate-y-4\.5:hover{--tw-translate-y:18px}.hover\:sl--translate-y-0:hover{--tw-translate-y:0px}.hover\:sl--translate-y-1:hover{--tw-translate-y:-4px}.hover\:sl--translate-y-2:hover{--tw-translate-y:-8px}.hover\:sl--translate-y-3:hover{--tw-translate-y:-12px}.hover\:sl--translate-y-4:hover{--tw-translate-y:-16px}.hover\:sl--translate-y-5:hover{--tw-translate-y:-20px}.hover\:sl--translate-y-6:hover{--tw-translate-y:-24px}.hover\:sl--translate-y-7:hover{--tw-translate-y:-28px}.hover\:sl--translate-y-8:hover{--tw-translate-y:-32px}.hover\:sl--translate-y-9:hover{--tw-translate-y:-36px}.hover\:sl--translate-y-10:hover{--tw-translate-y:-40px}.hover\:sl--translate-y-11:hover{--tw-translate-y:-44px}.hover\:sl--translate-y-12:hover{--tw-translate-y:-48px}.hover\:sl--translate-y-14:hover{--tw-translate-y:-56px}.hover\:sl--translate-y-16:hover{--tw-translate-y:-64px}.hover\:sl--translate-y-20:hover{--tw-translate-y:-80px}.hover\:sl--translate-y-24:hover{--tw-translate-y:-96px}.hover\:sl--translate-y-28:hover{--tw-translate-y:-112px}.hover\:sl--translate-y-32:hover{--tw-translate-y:-128px}.hover\:sl--translate-y-36:hover{--tw-translate-y:-144px}.hover\:sl--translate-y-40:hover{--tw-translate-y:-160px}.hover\:sl--translate-y-44:hover{--tw-translate-y:-176px}.hover\:sl--translate-y-48:hover{--tw-translate-y:-192px}.hover\:sl--translate-y-52:hover{--tw-translate-y:-208px}.hover\:sl--translate-y-56:hover{--tw-translate-y:-224px}.hover\:sl--translate-y-60:hover{--tw-translate-y:-240px}.hover\:sl--translate-y-64:hover{--tw-translate-y:-256px}.hover\:sl--translate-y-72:hover{--tw-translate-y:-288px}.hover\:sl--translate-y-80:hover{--tw-translate-y:-320px}.hover\:sl--translate-y-96:hover{--tw-translate-y:-384px}.hover\:sl--translate-y-px:hover{--tw-translate-y:-1px}.hover\:sl--translate-y-0\.5:hover{--tw-translate-y:-2px}.hover\:sl--translate-y-1\.5:hover{--tw-translate-y:-6px}.hover\:sl--translate-y-2\.5:hover{--tw-translate-y:-10px}.hover\:sl--translate-y-3\.5:hover{--tw-translate-y:-14px}.hover\:sl--translate-y-4\.5:hover{--tw-translate-y:-18px}.sl-select-none{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.sl-select-text{-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text}.sl-select-all{-webkit-user-select:all;-moz-user-select:all;user-select:all}.sl-select-auto{-webkit-user-select:auto;-moz-user-select:auto;-ms-user-select:auto;user-select:auto}.sl-align-baseline{vertical-align:baseline}.sl-align-top{vertical-align:top}.sl-align-middle{vertical-align:middle}.sl-align-bottom{vertical-align:bottom}.sl-align-text-top{vertical-align:text-top}.sl-align-text-bottom{vertical-align:text-bottom}.sl-visible{visibility:visible}.sl-invisible{visibility:hidden}.sl-group:hover .group-hover\:sl-visible{visibility:visible}.sl-group:hover .group-hover\:sl-invisible{visibility:hidden}.sl-group:focus .group-focus\:sl-visible{visibility:visible}.sl-group:focus .group-focus\:sl-invisible{visibility:hidden}.sl-whitespace-normal{white-space:normal}.sl-whitespace-nowrap{white-space:nowrap}.sl-whitespace-pre{white-space:pre}.sl-whitespace-pre-line{white-space:pre-line}.sl-whitespace-pre-wrap{white-space:pre-wrap}.sl-w-0{width:0}.sl-w-1{width:4px}.sl-w-2{width:8px}.sl-w-3{width:12px}.sl-w-4{width:16px}.sl-w-5{width:20px}.sl-w-6{width:24px}.sl-w-7{width:28px}.sl-w-8{width:32px}.sl-w-9{width:36px}.sl-w-10{width:40px}.sl-w-11{width:44px}.sl-w-12{width:48px}.sl-w-14{width:56px}.sl-w-16{width:64px}.sl-w-20{width:80px}.sl-w-24{width:96px}.sl-w-28{width:112px}.sl-w-32{width:128px}.sl-w-36{width:144px}.sl-w-40{width:160px}.sl-w-44{width:176px}.sl-w-48{width:192px}.sl-w-52{width:208px}.sl-w-56{width:224px}.sl-w-60{width:240px}.sl-w-64{width:256px}.sl-w-72{width:288px}.sl-w-80{width:320px}.sl-w-96{width:384px}.sl-w-auto{width:auto}.sl-w-px{width:1px}.sl-w-0\.5{width:2px}.sl-w-1\.5{width:6px}.sl-w-2\.5{width:10px}.sl-w-3\.5{width:14px}.sl-w-4\.5{width:18px}.sl-w-xs{width:20px}.sl-w-sm{width:24px}.sl-w-md{width:32px}.sl-w-lg{width:36px}.sl-w-xl{width:44px}.sl-w-2xl{width:52px}.sl-w-3xl{width:60px}.sl-w-1\/2{width:50%}.sl-w-1\/3{width:33.333333%}.sl-w-2\/3{width:66.666667%}.sl-w-1\/4{width:25%}.sl-w-2\/4{width:50%}.sl-w-3\/4{width:75%}.sl-w-1\/5{width:20%}.sl-w-2\/5{width:40%}.sl-w-3\/5{width:60%}.sl-w-4\/5{width:80%}.sl-w-1\/6{width:16.666667%}.sl-w-2\/6{width:33.333333%}.sl-w-3\/6{width:50%}.sl-w-4\/6{width:66.666667%}.sl-w-5\/6{width:83.333333%}.sl-w-full{width:100%}.sl-w-screen{width:100vw}.sl-w-min{width:-moz-min-content;width:min-content}.sl-w-max{width:-moz-max-content;width:max-content}.sl-break-normal{overflow-wrap:normal;word-break:normal}.sl-break-words{overflow-wrap:break-word}.sl-break-all{word-break:break-all}.sl-z-0{z-index:0}.sl-z-10{z-index:10}.sl-z-20{z-index:20}.sl-z-30{z-index:30}.sl-z-40{z-index:40}.sl-z-50{z-index:50}.sl-z-auto{z-index:auto}.focus\:sl-z-0:focus{z-index:0}.focus\:sl-z-10:focus{z-index:10}.focus\:sl-z-20:focus{z-index:20}.focus\:sl-z-30:focus{z-index:30}.focus\:sl-z-40:focus{z-index:40}.focus\:sl-z-50:focus{z-index:50}.focus\:sl-z-auto:focus{z-index:auto}:root{--font-prose:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-ui:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:"SF Mono",ui-monospace,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--font-code:var(--font-mono);--fs-paragraph-leading:22px;--fs-paragraph:16px;--fs-code:14px;--fs-paragraph-small:14px;--fs-paragraph-tiny:12px;--lh-paragraph-leading:1.875;--lh-paragraph:1.625;--lh-code:1.5;--lh-paragraph-small:1.625;--lh-paragraph-tiny:1.625;--color-code:var(--color-canvas-tint);--color-on-code:var(--color-text-heading)}.sl-avatar--with-bg:before{background-color:var(--avatar-bg-color);bottom:0;content:" ";left:0;opacity:var(--avatar-bg-opacity);position:absolute;right:0;top:0}.sl-aspect-ratio:before{content:"";display:block;height:0;padding-bottom:calc(1/var(--ratio)*100%)}.sl-aspect-ratio>:not(style){align-items:center;bottom:0;display:flex;height:100%;justify-content:center;left:0;overflow:hidden;position:absolute;right:0;top:0;width:100%}.sl-aspect-ratio>img,.sl-aspect-ratio>video{object-fit:cover}.sl-badge{align-items:center;border-width:1px;display:inline-flex;outline:2px solid transparent;outline-offset:2px}.sl-form-group.sl-badge{gap:1px}.sl-badge a{color:var(--color-text-muted)}.sl-badge a:hover{color:var(--color-text);cursor:pointer}.sl-button{align-items:center;display:inline-flex;line-height:0;outline:2px solid transparent;outline-offset:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sl-button-group>.sl-button:not(:first-child):not(:last-child){border-radius:0;border-right:0}.sl-button-group>.sl-button:first-child:not(:last-child){border-bottom-right-radius:0;border-right:0;border-top-right-radius:0}.sl-button-group>.sl-button:last-child:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.sl-form-group .sl-form-group-border{border-radius:0}.sl-form-group.sl-rounded-lg>:first-child.sl-form-group-border,.sl-form-group.sl-rounded-lg>:first-child .sl-form-group-border{border-bottom-left-radius:5px;border-top-left-radius:5px}.sl-form-group.sl-rounded-xl>:first-child.sl-form-group-border,.sl-form-group.sl-rounded-xl>:first-child .sl-form-group-border{border-bottom-left-radius:7px;border-top-left-radius:7px}.sl-form-group.sl-rounded-lg>:last-child.sl-form-group-border,.sl-form-group.sl-rounded-lg>:last-child .sl-form-group-border{border-bottom-right-radius:5px;border-top-right-radius:5px}.sl-form-group.sl-rounded-xl>:last-child.sl-form-group-border,.sl-form-group.sl-rounded-xl>:last-child .sl-form-group-border{border-bottom-right-radius:7px;border-top-right-radius:7px}.sl-form-group.sl-border{gap:1px}.sl-form-group.sl-border-2{gap:2px}.sl-form-group.sl-border-4{gap:4px}.sl-form-group.sl-border-8{gap:8px}.sl-form-group{background:var(--color-border,currentColor);border-color:transparent}.sl-form-group.sl-border-button{background:var(--color-border-button)}.sl-form-group.sl-border-input{background:var(--color-border-input)}.sl-form-group.sl-border-dark{background:var(--color-border-dark)}.sl-form-group.sl-border-light{background:var(--color-border-light)}.sl-form-group .sl-form-group-border.sl-bg-transparent{background:var(--color-canvas)}.sl-form-group :focus-within{z-index:1}.sl-image--inverted{filter:invert(1) hue-rotate(180deg);mix-blend-mode:screen}.Link{color:var(--color-link)}.Link>code{color:var(--color-link)}.Link:hover{color:var(--color-link-dark)}.Link:hover>code{color:var(--color-link-dark)}.sl-link-heading:hover .sl-link-heading__icon{opacity:1}.sl-link-heading__icon{opacity:0}.sl-menu{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.sl-menu--pointer-interactions .sl-menu-item:not(.sl-menu-item--disabled):hover{background-color:var(--color-primary);color:var(--color-on-primary)}.sl-menu--pointer-interactions .sl-menu-item:not(.sl-menu-item--disabled):hover .sl-menu-item__description{color:var(--color-on-primary)}.sl-menu--pointer-interactions .sl-menu-item:not(.sl-menu-item--disabled):hover .sl-menu-item__icon{color:var(--color-on-primary)!important}.sl-menu-item__link-icon,.sl-menu-item__meta-text{opacity:.6}.sl-menu-item--disabled .sl-menu-item__title-wrapper{cursor:not-allowed;opacity:.5}.sl-menu-item--disabled .sl-menu-item__meta-text{cursor:not-allowed;opacity:.4}.sl-menu-item--focused{background-color:var(--color-primary);color:var(--color-on-primary)}.sl-menu-item--focused .sl-menu-item__link-icon,.sl-menu-item--focused .sl-menu-item__meta-text{opacity:1}.sl-menu-item--focused .sl-menu-item__description{color:var(--color-on-primary)}.sl-menu-item--focused .sl-menu-item__icon{color:var(--color-on-primary)!important}.sl-menu-item--submenu-active{background-color:var(--color-primary-tint)}.sl-menu-item__title-wrapper{max-width:250px}.sl-menu-item__description{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.sl-popover{border-radius:2px}.sl-form-group.sl-popover>:first-child.sl-form-group-border,.sl-form-group.sl-popover>:first-child .sl-form-group-border{border-bottom-left-radius:2px;border-top-left-radius:2px}.sl-form-group.sl-popover>:last-child.sl-form-group-border,.sl-form-group.sl-popover>:last-child .sl-form-group-border{border-bottom-right-radius:2px;border-top-right-radius:2px}.sl-popover{--tw-blur:var(--tw-empty,/*!*/ /*!*/);--tw-brightness:var(--tw-empty,/*!*/ /*!*/);--tw-contrast:var(--tw-empty,/*!*/ /*!*/);--tw-grayscale:var(--tw-empty,/*!*/ /*!*/);--tw-hue-rotate:var(--tw-empty,/*!*/ /*!*/);--tw-invert:var(--tw-empty,/*!*/ /*!*/);--tw-saturate:var(--tw-empty,/*!*/ /*!*/);--tw-sepia:var(--tw-empty,/*!*/ /*!*/);--tw-drop-shadow:var(--tw-empty,/*!*/ /*!*/);--tw-drop-shadow:drop-shadow(var(--drop-shadow-default1)) drop-shadow(var(--drop-shadow-default2));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.sl-popover>:not(.sl-popover__tip){border-radius:2px;position:relative;z-index:10}.sl-popover .sl-form-group>:not(.sl-popover__tip)>:first-child.sl-form-group-border,.sl-popover .sl-form-group>:not(.sl-popover__tip)>:first-child .sl-form-group-border{border-bottom-left-radius:2px;border-top-left-radius:2px}.sl-popover .sl-form-group.sl-rounded>:first-child.sl-form-group-border,.sl-popover .sl-form-group.sl-rounded>:first-child .sl-form-group-border{border-bottom-left-radius:2px;border-top-left-radius:2px}.sl-popover .sl-form-group>:not(.sl-popover__tip)>:last-child.sl-form-group-border,.sl-popover .sl-form-group>:not(.sl-popover__tip)>:last-child .sl-form-group-border{border-bottom-right-radius:2px;border-top-right-radius:2px}.sl-popover .sl-form-group.sl-rounded>:last-child.sl-form-group-border,.sl-popover .sl-form-group.sl-rounded>:last-child .sl-form-group-border{border-bottom-right-radius:2px;border-top-right-radius:2px}.sl-prose{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;--fs-paragraph:1em;--fs-paragraph-small:0.875em;--fs-code:0.875em;font-family:var(--font-prose);font-size:16px;line-height:var(--lh-paragraph)}.sl-prose>:first-child{margin-top:0}.sl-prose>:last-child{margin-bottom:0}.sl-prose h1{font-size:2.25em}.sl-prose>h1{margin-bottom:1.11em;margin-top:0}.sl-prose h2{font-size:1.75em;line-height:1.3333333}.sl-prose>h2{margin-bottom:1em;margin-top:1.428em}.sl-prose h3{font-size:1.25em}.sl-prose>h3{margin-bottom:.8em;margin-top:2em}.sl-prose h4{font-size:1em}.sl-prose>h4{margin-bottom:.5em;margin-top:2em}.sl-prose h2+*,.sl-prose h3+*,.sl-prose h4+*{margin-top:0}.sl-prose strong{font-weight:600}.sl-prose .sl-text-lg{font-size:.875em}.sl-prose p{color:var(--color-text-paragraph);font-size:var(--fs-paragraph);margin-bottom:1em;margin-top:1em}.sl-prose p:first-child{margin-top:0}.sl-prose p:last-child{margin-bottom:0}.sl-prose p>a>img{display:inline}.sl-prose caption a,.sl-prose figcaption a,.sl-prose li a,.sl-prose p a,.sl-prose table a{color:var(--color-link)}.sl-prose caption a:hover,.sl-prose figcaption a:hover,.sl-prose li a:hover,.sl-prose p a:hover,.sl-prose table a:hover{color:var(--color-link-dark)}.sl-prose caption a,.sl-prose figcaption a,.sl-prose li a,.sl-prose p a,.sl-prose table a{--color-link:var(--color-text-primary);--color-link-dark:var(--color-primary-dark)}.sl-prose hr{margin-bottom:1em;margin-top:1em}.sl-prose .sl-live-code{margin:1.25em -4px;table-layout:auto;width:100%}.sl-prose .sl-live-code__inner>pre{margin-bottom:0;margin-top:0}.sl-prose .sl-callout,.sl-prose ol,.sl-prose ul{margin-bottom:1.5em;margin-top:1.5em}.sl-prose ol,.sl-prose ul{line-height:var(--lh-paragraph)}.sl-prose ol li,.sl-prose ul li{padding-left:2em}.sl-prose ol>li{counter-increment:sublist;position:relative}.sl-prose ol>li:before{content:counter(sublist) ". ";font-variant-numeric:tabular-nums}.sl-prose ol ol{counter-reset:sublist}.sl-prose ul:not(.contains-task-list)>li,.sl-prose ul:not(.contains-task-list)>ol>li{padding-left:3.9em;position:relative}.sl-prose ul li{left:-.9em}.sl-prose ul ul li{left:-1.9em}.sl-prose ul:not(.contains-task-list)>li:before,.sl-prose ul:not(.contains-task-list)>ol>li:before{background-color:var(--color-text);opacity:.7}.sl-prose ul:not(.contains-task-list)>li:before,.sl-prose ul:not(.contains-task-list)>ol>li:before{border-radius:50%;content:"";height:.375em;left:3.1em;position:absolute;top:.625em;width:.375em}.sl-prose li{margin-bottom:4px;margin-top:4px;padding-left:1.75em}.sl-prose li p{display:inline;margin-bottom:.75em;margin-top:.75em}.sl-prose li>:first-child{margin-top:0}.sl-prose>ul p+:last-child{margin-bottom:.75em}.sl-prose>ol p+:last-child{margin-bottom:.75em}.sl-prose ol ol,.sl-prose ol ul,.sl-prose ul ol,.sl-prose ul ul{margin-bottom:2px;margin-top:2px}.sl-prose ul.contains-task-list input{margin-left:-1.875em;margin-right:.625em;position:relative;top:1px}.sl-prose ul.contains-task-list p{margin-top:0}.sl-prose figure{margin-bottom:1.5em;margin-top:1.5em}.sl-prose figure figure,.sl-prose figure img,.sl-prose figure video{margin-bottom:0;margin-top:0}.sl-prose figure>figcaption{color:var(--color-text-muted);font-size:var(--fs-paragraph-small);line-height:var(--lh-paragraph-small);margin-top:8px;padding-left:16px;padding-right:16px;text-align:center}.sl-prose figure>figcaption p{color:var(--color-text-muted);font-size:var(--fs-paragraph-small);line-height:var(--lh-paragraph-small);margin-top:8px;padding-left:16px;padding-right:16px;text-align:center}.sl-prose blockquote p{margin-bottom:.5em;margin-top:.5em}.sl-prose table{font-size:var(--fs-paragraph-small);margin-bottom:1.5em;margin-left:-4px;margin-right:-4px;overflow-x:auto;table-layout:auto;width:100%}.sl-prose thead td,.sl-prose thead th{color:var(--color-text-muted);font-size:.857em;font-weight:500;padding:8px 12px;text-transform:uppercase}.sl-prose thead td:first-child,.sl-prose thead th:first-child{padding-left:4px}.sl-prose tbody{border-radius:5px}.sl-prose .sl-form-grouptbody>:first-child.sl-form-group-border,.sl-prose .sl-form-grouptbody>:first-child .sl-form-group-border{border-bottom-left-radius:5px;border-top-left-radius:5px}.sl-prose .sl-form-group.sl-rounded-lg>:first-child.sl-form-group-border,.sl-prose .sl-form-group.sl-rounded-lg>:first-child .sl-form-group-border{border-bottom-left-radius:5px;border-top-left-radius:5px}.sl-prose .sl-form-grouptbody>:last-child.sl-form-group-border,.sl-prose .sl-form-grouptbody>:last-child .sl-form-group-border{border-bottom-right-radius:5px;border-top-right-radius:5px}.sl-prose .sl-form-group.sl-rounded-lg>:last-child.sl-form-group-border,.sl-prose .sl-form-group.sl-rounded-lg>:last-child .sl-form-group-border{border-bottom-right-radius:5px;border-top-right-radius:5px}.sl-prose tbody{box-shadow:0 0 0 1px var(--color-border,currentColor)}.sl-prose tbody tr{border-top-width:1px}.sl-prose tbody tr:first-child{border-top:0}.sl-prose tbody tr:nth-child(2n){background-color:var(--color-canvas-tint)}.sl-prose td{margin:.625em .75em;padding:10px 12px;vertical-align:top}.sl-prose td:not([align=center],[align=right]),.sl-prose th:not([align=center],[align=right]){text-align:left}.sl-prose .mermaid{margin-bottom:1.5em;margin-top:1.5em}.sl-prose .mermaid>svg{border-radius:5px;border-width:1px}.sl-prose .mermaid .sl-form-group>svg>:first-child.sl-form-group-border,.sl-prose .mermaid .sl-form-group>svg>:first-child .sl-form-group-border{border-bottom-left-radius:5px;border-top-left-radius:5px}.sl-prose .mermaid .sl-form-group.sl-rounded-lg>:first-child.sl-form-group-border,.sl-prose .mermaid .sl-form-group.sl-rounded-lg>:first-child .sl-form-group-border{border-bottom-left-radius:5px;border-top-left-radius:5px}.sl-prose .mermaid .sl-form-group>svg>:last-child.sl-form-group-border,.sl-prose .mermaid .sl-form-group>svg>:last-child .sl-form-group-border{border-bottom-right-radius:5px;border-top-right-radius:5px}.sl-prose .mermaid .sl-form-group.sl-rounded-lg>:last-child.sl-form-group-border,.sl-prose .mermaid .sl-form-group.sl-rounded-lg>:last-child .sl-form-group-border{border-bottom-right-radius:5px;border-top-right-radius:5px}.sl-prose .mermaid .sl-form-group>svg{gap:1px}.sl-prose .mermaid>svg{height:auto!important;padding:1.25em}.sl-prose .sl-code-group .mermaid,.sl-prose .sl-code-group pre{margin-top:0}.sl-svg-focus{filter:drop-shadow(0 0 1px hsla(var(--primary-h),80%,51%,.9))}.sl-radio-group__radio:hover{cursor:pointer}.sl-radio-group__radio--disabled{opacity:.6}.sl-radio-group__radio--disabled:hover{cursor:not-allowed}.sl-switch .sl-switch__indicator{transition:background-color .1s cubic-bezier(.4,1,.75,.9)}.sl-switch .sl-switch__indicator .sl-switch__icon{visibility:hidden}.sl-switch .sl-switch__indicator:before{background-color:var(--color-canvas);border-radius:50%;content:"";height:calc(100% - 4px);left:0;margin:2px;position:absolute;transition:left .1s cubic-bezier(.4,1,.75,.9);width:calc(50% - 4px)}.sl-switch input:checked:disabled~.sl-switch__indicator{background-color:var(--color-primary-light)}.sl-switch input:checked~.sl-switch__indicator{background-color:var(--color-primary)}.sl-switch input:checked~.sl-switch__indicator .sl-switch__icon{visibility:visible}.sl-switch input:checked~.sl-switch__indicator:before{left:50%}.sl-tooltip{border-radius:2px;font-size:11px;padding:4px 6px}.sl-form-group.sl-tooltip>:first-child.sl-form-group-border,.sl-form-group.sl-tooltip>:first-child .sl-form-group-border{border-bottom-left-radius:2px;border-top-left-radius:2px}.sl-form-group.sl-tooltip>:last-child.sl-form-group-border,.sl-form-group.sl-tooltip>:last-child .sl-form-group-border{border-bottom-right-radius:2px;border-top-right-radius:2px}.sl-tooltip{--tw-blur:var(--tw-empty,/*!*/ /*!*/);--tw-brightness:var(--tw-empty,/*!*/ /*!*/);--tw-contrast:var(--tw-empty,/*!*/ /*!*/);--tw-grayscale:var(--tw-empty,/*!*/ /*!*/);--tw-hue-rotate:var(--tw-empty,/*!*/ /*!*/);--tw-invert:var(--tw-empty,/*!*/ /*!*/);--tw-saturate:var(--tw-empty,/*!*/ /*!*/);--tw-sepia:var(--tw-empty,/*!*/ /*!*/);--tw-drop-shadow:var(--tw-empty,/*!*/ /*!*/);--tw-drop-shadow:drop-shadow(var(--drop-shadow-default1)) drop-shadow(var(--drop-shadow-default2));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);max-width:300px}.sl-tooltip>:not(.sl-tooltip_tip){position:relative;z-index:10}.sl-drawer{overflow:auto;transition-property:transform}.sl-drawer.left{left:0;top:0;transform:translateX(-105%)}.sl-drawer.right{right:0;top:0;transform:translateX(100%)}.sl-drawer.top{left:0;right:0;top:0;transform:translateY(-100%)}.sl-drawer.bottom{bottom:0;left:0;right:0;transform:translateY(100%)}.sl-drawer-container.in.open .left,.sl-drawer-container.in.open .right{transform:translateX(0)}.sl-drawer-container.in.open .bottom,.sl-drawer-container.in.open .top{transform:translateY(0)}input,textarea{background-color:transparent}.sl-focus-ring{--tw-ring-color:hsla(var(--primary-h),80%,61%,var(--tw-ring-opacity)) ;--tw-ring-opacity:0.5;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);border-radius:2px;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.sl-form-group.sl-focus-ring>:first-child.sl-form-group-border,.sl-form-group.sl-focus-ring>:first-child .sl-form-group-border{border-bottom-left-radius:2px;border-top-left-radius:2px}.sl-form-group.sl-rounded>:first-child.sl-form-group-border,.sl-form-group.sl-rounded>:first-child .sl-form-group-border{border-bottom-left-radius:2px;border-top-left-radius:2px}.sl-form-group.sl-focus-ring>:last-child.sl-form-group-border,.sl-form-group.sl-focus-ring>:last-child .sl-form-group-border{border-bottom-right-radius:2px;border-top-right-radius:2px}.sl-form-group.sl-rounded>:last-child.sl-form-group-border,.sl-form-group.sl-rounded>:last-child .sl-form-group-border{border-bottom-right-radius:2px;border-top-right-radius:2px}@media (max-width:479px){.sm\:sl-stack--1{gap:4px}.sm\:sl-stack--2{gap:8px}.sm\:sl-stack--3{gap:12px}.sm\:sl-stack--4{gap:16px}.sm\:sl-stack--5{gap:20px}.sm\:sl-stack--6{gap:24px}.sm\:sl-stack--7{gap:28px}.sm\:sl-stack--8{gap:32px}.sm\:sl-stack--9{gap:36px}.sm\:sl-stack--10{gap:40px}.sm\:sl-stack--12{gap:48px}.sm\:sl-stack--14{gap:56px}.sm\:sl-stack--16{gap:64px}.sm\:sl-stack--20{gap:80px}.sm\:sl-stack--24{gap:96px}.sm\:sl-stack--32{gap:128px}.sm\:sl-content-center{align-content:center}.sm\:sl-content-start{align-content:flex-start}.sm\:sl-content-end{align-content:flex-end}.sm\:sl-content-between{align-content:space-between}.sm\:sl-content-around{align-content:space-around}.sm\:sl-content-evenly{align-content:space-evenly}.sm\:sl-items-start{align-items:flex-start}.sm\:sl-items-end{align-items:flex-end}.sm\:sl-items-center{align-items:center}.sm\:sl-items-baseline{align-items:baseline}.sm\:sl-items-stretch{align-items:stretch}.sm\:sl-self-auto{align-self:auto}.sm\:sl-self-start{align-self:flex-start}.sm\:sl-self-end{align-self:flex-end}.sm\:sl-self-center{align-self:center}.sm\:sl-self-stretch{align-self:stretch}.sm\:sl-blur-0,.sm\:sl-blur-none{--tw-blur:blur(0)}.sm\:sl-blur-sm{--tw-blur:blur(4px)}.sm\:sl-blur{--tw-blur:blur(8px)}.sm\:sl-blur-md{--tw-blur:blur(12px)}.sm\:sl-blur-lg{--tw-blur:blur(16px)}.sm\:sl-blur-xl{--tw-blur:blur(24px)}.sm\:sl-blur-2xl{--tw-blur:blur(40px)}.sm\:sl-blur-3xl{--tw-blur:blur(64px)}.sm\:sl-block{display:block}.sm\:sl-inline-block{display:inline-block}.sm\:sl-inline{display:inline}.sm\:sl-flex{display:flex}.sm\:sl-inline-flex{display:inline-flex}.sm\:sl-table{display:table}.sm\:sl-inline-table{display:inline-table}.sm\:sl-table-caption{display:table-caption}.sm\:sl-table-cell{display:table-cell}.sm\:sl-table-column{display:table-column}.sm\:sl-table-column-group{display:table-column-group}.sm\:sl-table-footer-group{display:table-footer-group}.sm\:sl-table-header-group{display:table-header-group}.sm\:sl-table-row-group{display:table-row-group}.sm\:sl-table-row{display:table-row}.sm\:sl-flow-root{display:flow-root}.sm\:sl-grid{display:grid}.sm\:sl-inline-grid{display:inline-grid}.sm\:sl-contents{display:contents}.sm\:sl-list-item{display:list-item}.sm\:sl-hidden{display:none}.sm\:sl-drop-shadow{--tw-drop-shadow:drop-shadow(var(--drop-shadow-default1)) drop-shadow(var(--drop-shadow-default2))}.sm\:sl-flex-1{flex:1 1}.sm\:sl-flex-auto{flex:1 1 auto}.sm\:sl-flex-initial{flex:0 1 auto}.sm\:sl-flex-none{flex:none}.sm\:sl-flex-row{flex-direction:row}.sm\:sl-flex-row-reverse{flex-direction:row-reverse}.sm\:sl-flex-col{flex-direction:column}.sm\:sl-flex-col-reverse{flex-direction:column-reverse}.sm\:sl-flex-grow-0{flex-grow:0}.sm\:sl-flex-grow{flex-grow:1}.sm\:sl-flex-shrink-0{flex-shrink:0}.sm\:sl-flex-shrink{flex-shrink:1}.sm\:sl-flex-wrap{flex-wrap:wrap}.sm\:sl-flex-wrap-reverse{flex-wrap:wrap-reverse}.sm\:sl-flex-nowrap{flex-wrap:nowrap}.sm\:sl-h-0{height:0}.sm\:sl-h-1{height:4px}.sm\:sl-h-2{height:8px}.sm\:sl-h-3{height:12px}.sm\:sl-h-4{height:16px}.sm\:sl-h-5{height:20px}.sm\:sl-h-6{height:24px}.sm\:sl-h-7{height:28px}.sm\:sl-h-8{height:32px}.sm\:sl-h-9{height:36px}.sm\:sl-h-10{height:40px}.sm\:sl-h-11{height:44px}.sm\:sl-h-12{height:48px}.sm\:sl-h-14{height:56px}.sm\:sl-h-16{height:64px}.sm\:sl-h-20{height:80px}.sm\:sl-h-24{height:96px}.sm\:sl-h-28{height:112px}.sm\:sl-h-32{height:128px}.sm\:sl-h-36{height:144px}.sm\:sl-h-40{height:160px}.sm\:sl-h-44{height:176px}.sm\:sl-h-48{height:192px}.sm\:sl-h-52{height:208px}.sm\:sl-h-56{height:224px}.sm\:sl-h-60{height:240px}.sm\:sl-h-64{height:256px}.sm\:sl-h-72{height:288px}.sm\:sl-h-80{height:320px}.sm\:sl-h-96{height:384px}.sm\:sl-h-auto{height:auto}.sm\:sl-h-px{height:1px}.sm\:sl-h-0\.5{height:2px}.sm\:sl-h-1\.5{height:6px}.sm\:sl-h-2\.5{height:10px}.sm\:sl-h-3\.5{height:14px}.sm\:sl-h-4\.5{height:18px}.sm\:sl-h-xs{height:20px}.sm\:sl-h-sm{height:24px}.sm\:sl-h-md{height:32px}.sm\:sl-h-lg{height:36px}.sm\:sl-h-xl{height:44px}.sm\:sl-h-2xl{height:52px}.sm\:sl-h-3xl{height:60px}.sm\:sl-h-full{height:100%}.sm\:sl-h-screen{height:100vh}.sm\:sl-justify-start{justify-content:flex-start}.sm\:sl-justify-end{justify-content:flex-end}.sm\:sl-justify-center{justify-content:center}.sm\:sl-justify-between{justify-content:space-between}.sm\:sl-justify-around{justify-content:space-around}.sm\:sl-justify-evenly{justify-content:space-evenly}.sm\:sl-justify-items-start{justify-items:start}.sm\:sl-justify-items-end{justify-items:end}.sm\:sl-justify-items-center{justify-items:center}.sm\:sl-justify-items-stretch{justify-items:stretch}.sm\:sl-justify-self-auto{justify-self:auto}.sm\:sl-justify-self-start{justify-self:start}.sm\:sl-justify-self-end{justify-self:end}.sm\:sl-justify-self-center{justify-self:center}.sm\:sl-justify-self-stretch{justify-self:stretch}.sm\:sl-m-0{margin:0}.sm\:sl-m-1{margin:4px}.sm\:sl-m-2{margin:8px}.sm\:sl-m-3{margin:12px}.sm\:sl-m-4{margin:16px}.sm\:sl-m-5{margin:20px}.sm\:sl-m-6{margin:24px}.sm\:sl-m-7{margin:28px}.sm\:sl-m-8{margin:32px}.sm\:sl-m-9{margin:36px}.sm\:sl-m-10{margin:40px}.sm\:sl-m-11{margin:44px}.sm\:sl-m-12{margin:48px}.sm\:sl-m-14{margin:56px}.sm\:sl-m-16{margin:64px}.sm\:sl-m-20{margin:80px}.sm\:sl-m-24{margin:96px}.sm\:sl-m-28{margin:112px}.sm\:sl-m-32{margin:128px}.sm\:sl-m-36{margin:144px}.sm\:sl-m-40{margin:160px}.sm\:sl-m-44{margin:176px}.sm\:sl-m-48{margin:192px}.sm\:sl-m-52{margin:208px}.sm\:sl-m-56{margin:224px}.sm\:sl-m-60{margin:240px}.sm\:sl-m-64{margin:256px}.sm\:sl-m-72{margin:288px}.sm\:sl-m-80{margin:320px}.sm\:sl-m-96{margin:384px}.sm\:sl-m-auto{margin:auto}.sm\:sl-m-px{margin:1px}.sm\:sl-m-0\.5{margin:2px}.sm\:sl-m-1\.5{margin:6px}.sm\:sl-m-2\.5{margin:10px}.sm\:sl-m-3\.5{margin:14px}.sm\:sl-m-4\.5{margin:18px}.sm\:sl--m-0{margin:0}.sm\:sl--m-1{margin:-4px}.sm\:sl--m-2{margin:-8px}.sm\:sl--m-3{margin:-12px}.sm\:sl--m-4{margin:-16px}.sm\:sl--m-5{margin:-20px}.sm\:sl--m-6{margin:-24px}.sm\:sl--m-7{margin:-28px}.sm\:sl--m-8{margin:-32px}.sm\:sl--m-9{margin:-36px}.sm\:sl--m-10{margin:-40px}.sm\:sl--m-11{margin:-44px}.sm\:sl--m-12{margin:-48px}.sm\:sl--m-14{margin:-56px}.sm\:sl--m-16{margin:-64px}.sm\:sl--m-20{margin:-80px}.sm\:sl--m-24{margin:-96px}.sm\:sl--m-28{margin:-112px}.sm\:sl--m-32{margin:-128px}.sm\:sl--m-36{margin:-144px}.sm\:sl--m-40{margin:-160px}.sm\:sl--m-44{margin:-176px}.sm\:sl--m-48{margin:-192px}.sm\:sl--m-52{margin:-208px}.sm\:sl--m-56{margin:-224px}.sm\:sl--m-60{margin:-240px}.sm\:sl--m-64{margin:-256px}.sm\:sl--m-72{margin:-288px}.sm\:sl--m-80{margin:-320px}.sm\:sl--m-96{margin:-384px}.sm\:sl--m-px{margin:-1px}.sm\:sl--m-0\.5{margin:-2px}.sm\:sl--m-1\.5{margin:-6px}.sm\:sl--m-2\.5{margin:-10px}.sm\:sl--m-3\.5{margin:-14px}.sm\:sl--m-4\.5{margin:-18px}.sm\:sl-my-0{margin-bottom:0;margin-top:0}.sm\:sl-mx-0{margin-left:0;margin-right:0}.sm\:sl-my-1{margin-bottom:4px;margin-top:4px}.sm\:sl-mx-1{margin-left:4px;margin-right:4px}.sm\:sl-my-2{margin-bottom:8px;margin-top:8px}.sm\:sl-mx-2{margin-left:8px;margin-right:8px}.sm\:sl-my-3{margin-bottom:12px;margin-top:12px}.sm\:sl-mx-3{margin-left:12px;margin-right:12px}.sm\:sl-my-4{margin-bottom:16px;margin-top:16px}.sm\:sl-mx-4{margin-left:16px;margin-right:16px}.sm\:sl-my-5{margin-bottom:20px;margin-top:20px}.sm\:sl-mx-5{margin-left:20px;margin-right:20px}.sm\:sl-my-6{margin-bottom:24px;margin-top:24px}.sm\:sl-mx-6{margin-left:24px;margin-right:24px}.sm\:sl-my-7{margin-bottom:28px;margin-top:28px}.sm\:sl-mx-7{margin-left:28px;margin-right:28px}.sm\:sl-my-8{margin-bottom:32px;margin-top:32px}.sm\:sl-mx-8{margin-left:32px;margin-right:32px}.sm\:sl-my-9{margin-bottom:36px;margin-top:36px}.sm\:sl-mx-9{margin-left:36px;margin-right:36px}.sm\:sl-my-10{margin-bottom:40px;margin-top:40px}.sm\:sl-mx-10{margin-left:40px;margin-right:40px}.sm\:sl-my-11{margin-bottom:44px;margin-top:44px}.sm\:sl-mx-11{margin-left:44px;margin-right:44px}.sm\:sl-my-12{margin-bottom:48px;margin-top:48px}.sm\:sl-mx-12{margin-left:48px;margin-right:48px}.sm\:sl-my-14{margin-bottom:56px;margin-top:56px}.sm\:sl-mx-14{margin-left:56px;margin-right:56px}.sm\:sl-my-16{margin-bottom:64px;margin-top:64px}.sm\:sl-mx-16{margin-left:64px;margin-right:64px}.sm\:sl-my-20{margin-bottom:80px;margin-top:80px}.sm\:sl-mx-20{margin-left:80px;margin-right:80px}.sm\:sl-my-24{margin-bottom:96px;margin-top:96px}.sm\:sl-mx-24{margin-left:96px;margin-right:96px}.sm\:sl-my-28{margin-bottom:112px;margin-top:112px}.sm\:sl-mx-28{margin-left:112px;margin-right:112px}.sm\:sl-my-32{margin-bottom:128px;margin-top:128px}.sm\:sl-mx-32{margin-left:128px;margin-right:128px}.sm\:sl-my-36{margin-bottom:144px;margin-top:144px}.sm\:sl-mx-36{margin-left:144px;margin-right:144px}.sm\:sl-my-40{margin-bottom:160px;margin-top:160px}.sm\:sl-mx-40{margin-left:160px;margin-right:160px}.sm\:sl-my-44{margin-bottom:176px;margin-top:176px}.sm\:sl-mx-44{margin-left:176px;margin-right:176px}.sm\:sl-my-48{margin-bottom:192px;margin-top:192px}.sm\:sl-mx-48{margin-left:192px;margin-right:192px}.sm\:sl-my-52{margin-bottom:208px;margin-top:208px}.sm\:sl-mx-52{margin-left:208px;margin-right:208px}.sm\:sl-my-56{margin-bottom:224px;margin-top:224px}.sm\:sl-mx-56{margin-left:224px;margin-right:224px}.sm\:sl-my-60{margin-bottom:240px;margin-top:240px}.sm\:sl-mx-60{margin-left:240px;margin-right:240px}.sm\:sl-my-64{margin-bottom:256px;margin-top:256px}.sm\:sl-mx-64{margin-left:256px;margin-right:256px}.sm\:sl-my-72{margin-bottom:288px;margin-top:288px}.sm\:sl-mx-72{margin-left:288px;margin-right:288px}.sm\:sl-my-80{margin-bottom:320px;margin-top:320px}.sm\:sl-mx-80{margin-left:320px;margin-right:320px}.sm\:sl-my-96{margin-bottom:384px;margin-top:384px}.sm\:sl-mx-96{margin-left:384px;margin-right:384px}.sm\:sl-my-auto{margin-bottom:auto;margin-top:auto}.sm\:sl-mx-auto{margin-left:auto;margin-right:auto}.sm\:sl-my-px{margin-bottom:1px;margin-top:1px}.sm\:sl-mx-px{margin-left:1px;margin-right:1px}.sm\:sl-my-0\.5{margin-bottom:2px;margin-top:2px}.sm\:sl-mx-0\.5{margin-left:2px;margin-right:2px}.sm\:sl-my-1\.5{margin-bottom:6px;margin-top:6px}.sm\:sl-mx-1\.5{margin-left:6px;margin-right:6px}.sm\:sl-my-2\.5{margin-bottom:10px;margin-top:10px}.sm\:sl-mx-2\.5{margin-left:10px;margin-right:10px}.sm\:sl-my-3\.5{margin-bottom:14px;margin-top:14px}.sm\:sl-mx-3\.5{margin-left:14px;margin-right:14px}.sm\:sl-my-4\.5{margin-bottom:18px;margin-top:18px}.sm\:sl-mx-4\.5{margin-left:18px;margin-right:18px}.sm\:sl--my-0{margin-bottom:0;margin-top:0}.sm\:sl--mx-0{margin-left:0;margin-right:0}.sm\:sl--my-1{margin-bottom:-4px;margin-top:-4px}.sm\:sl--mx-1{margin-left:-4px;margin-right:-4px}.sm\:sl--my-2{margin-bottom:-8px;margin-top:-8px}.sm\:sl--mx-2{margin-left:-8px;margin-right:-8px}.sm\:sl--my-3{margin-bottom:-12px;margin-top:-12px}.sm\:sl--mx-3{margin-left:-12px;margin-right:-12px}.sm\:sl--my-4{margin-bottom:-16px;margin-top:-16px}.sm\:sl--mx-4{margin-left:-16px;margin-right:-16px}.sm\:sl--my-5{margin-bottom:-20px;margin-top:-20px}.sm\:sl--mx-5{margin-left:-20px;margin-right:-20px}.sm\:sl--my-6{margin-bottom:-24px;margin-top:-24px}.sm\:sl--mx-6{margin-left:-24px;margin-right:-24px}.sm\:sl--my-7{margin-bottom:-28px;margin-top:-28px}.sm\:sl--mx-7{margin-left:-28px;margin-right:-28px}.sm\:sl--my-8{margin-bottom:-32px;margin-top:-32px}.sm\:sl--mx-8{margin-left:-32px;margin-right:-32px}.sm\:sl--my-9{margin-bottom:-36px;margin-top:-36px}.sm\:sl--mx-9{margin-left:-36px;margin-right:-36px}.sm\:sl--my-10{margin-bottom:-40px;margin-top:-40px}.sm\:sl--mx-10{margin-left:-40px;margin-right:-40px}.sm\:sl--my-11{margin-bottom:-44px;margin-top:-44px}.sm\:sl--mx-11{margin-left:-44px;margin-right:-44px}.sm\:sl--my-12{margin-bottom:-48px;margin-top:-48px}.sm\:sl--mx-12{margin-left:-48px;margin-right:-48px}.sm\:sl--my-14{margin-bottom:-56px;margin-top:-56px}.sm\:sl--mx-14{margin-left:-56px;margin-right:-56px}.sm\:sl--my-16{margin-bottom:-64px;margin-top:-64px}.sm\:sl--mx-16{margin-left:-64px;margin-right:-64px}.sm\:sl--my-20{margin-bottom:-80px;margin-top:-80px}.sm\:sl--mx-20{margin-left:-80px;margin-right:-80px}.sm\:sl--my-24{margin-bottom:-96px;margin-top:-96px}.sm\:sl--mx-24{margin-left:-96px;margin-right:-96px}.sm\:sl--my-28{margin-bottom:-112px;margin-top:-112px}.sm\:sl--mx-28{margin-left:-112px;margin-right:-112px}.sm\:sl--my-32{margin-bottom:-128px;margin-top:-128px}.sm\:sl--mx-32{margin-left:-128px;margin-right:-128px}.sm\:sl--my-36{margin-bottom:-144px;margin-top:-144px}.sm\:sl--mx-36{margin-left:-144px;margin-right:-144px}.sm\:sl--my-40{margin-bottom:-160px;margin-top:-160px}.sm\:sl--mx-40{margin-left:-160px;margin-right:-160px}.sm\:sl--my-44{margin-bottom:-176px;margin-top:-176px}.sm\:sl--mx-44{margin-left:-176px;margin-right:-176px}.sm\:sl--my-48{margin-bottom:-192px;margin-top:-192px}.sm\:sl--mx-48{margin-left:-192px;margin-right:-192px}.sm\:sl--my-52{margin-bottom:-208px;margin-top:-208px}.sm\:sl--mx-52{margin-left:-208px;margin-right:-208px}.sm\:sl--my-56{margin-bottom:-224px;margin-top:-224px}.sm\:sl--mx-56{margin-left:-224px;margin-right:-224px}.sm\:sl--my-60{margin-bottom:-240px;margin-top:-240px}.sm\:sl--mx-60{margin-left:-240px;margin-right:-240px}.sm\:sl--my-64{margin-bottom:-256px;margin-top:-256px}.sm\:sl--mx-64{margin-left:-256px;margin-right:-256px}.sm\:sl--my-72{margin-bottom:-288px;margin-top:-288px}.sm\:sl--mx-72{margin-left:-288px;margin-right:-288px}.sm\:sl--my-80{margin-bottom:-320px;margin-top:-320px}.sm\:sl--mx-80{margin-left:-320px;margin-right:-320px}.sm\:sl--my-96{margin-bottom:-384px;margin-top:-384px}.sm\:sl--mx-96{margin-left:-384px;margin-right:-384px}.sm\:sl--my-px{margin-bottom:-1px;margin-top:-1px}.sm\:sl--mx-px{margin-left:-1px;margin-right:-1px}.sm\:sl--my-0\.5{margin-bottom:-2px;margin-top:-2px}.sm\:sl--mx-0\.5{margin-left:-2px;margin-right:-2px}.sm\:sl--my-1\.5{margin-bottom:-6px;margin-top:-6px}.sm\:sl--mx-1\.5{margin-left:-6px;margin-right:-6px}.sm\:sl--my-2\.5{margin-bottom:-10px;margin-top:-10px}.sm\:sl--mx-2\.5{margin-left:-10px;margin-right:-10px}.sm\:sl--my-3\.5{margin-bottom:-14px;margin-top:-14px}.sm\:sl--mx-3\.5{margin-left:-14px;margin-right:-14px}.sm\:sl--my-4\.5{margin-bottom:-18px;margin-top:-18px}.sm\:sl--mx-4\.5{margin-left:-18px;margin-right:-18px}.sm\:sl-mt-0{margin-top:0}.sm\:sl-mr-0{margin-right:0}.sm\:sl-mb-0{margin-bottom:0}.sm\:sl-ml-0{margin-left:0}.sm\:sl-mt-1{margin-top:4px}.sm\:sl-mr-1{margin-right:4px}.sm\:sl-mb-1{margin-bottom:4px}.sm\:sl-ml-1{margin-left:4px}.sm\:sl-mt-2{margin-top:8px}.sm\:sl-mr-2{margin-right:8px}.sm\:sl-mb-2{margin-bottom:8px}.sm\:sl-ml-2{margin-left:8px}.sm\:sl-mt-3{margin-top:12px}.sm\:sl-mr-3{margin-right:12px}.sm\:sl-mb-3{margin-bottom:12px}.sm\:sl-ml-3{margin-left:12px}.sm\:sl-mt-4{margin-top:16px}.sm\:sl-mr-4{margin-right:16px}.sm\:sl-mb-4{margin-bottom:16px}.sm\:sl-ml-4{margin-left:16px}.sm\:sl-mt-5{margin-top:20px}.sm\:sl-mr-5{margin-right:20px}.sm\:sl-mb-5{margin-bottom:20px}.sm\:sl-ml-5{margin-left:20px}.sm\:sl-mt-6{margin-top:24px}.sm\:sl-mr-6{margin-right:24px}.sm\:sl-mb-6{margin-bottom:24px}.sm\:sl-ml-6{margin-left:24px}.sm\:sl-mt-7{margin-top:28px}.sm\:sl-mr-7{margin-right:28px}.sm\:sl-mb-7{margin-bottom:28px}.sm\:sl-ml-7{margin-left:28px}.sm\:sl-mt-8{margin-top:32px}.sm\:sl-mr-8{margin-right:32px}.sm\:sl-mb-8{margin-bottom:32px}.sm\:sl-ml-8{margin-left:32px}.sm\:sl-mt-9{margin-top:36px}.sm\:sl-mr-9{margin-right:36px}.sm\:sl-mb-9{margin-bottom:36px}.sm\:sl-ml-9{margin-left:36px}.sm\:sl-mt-10{margin-top:40px}.sm\:sl-mr-10{margin-right:40px}.sm\:sl-mb-10{margin-bottom:40px}.sm\:sl-ml-10{margin-left:40px}.sm\:sl-mt-11{margin-top:44px}.sm\:sl-mr-11{margin-right:44px}.sm\:sl-mb-11{margin-bottom:44px}.sm\:sl-ml-11{margin-left:44px}.sm\:sl-mt-12{margin-top:48px}.sm\:sl-mr-12{margin-right:48px}.sm\:sl-mb-12{margin-bottom:48px}.sm\:sl-ml-12{margin-left:48px}.sm\:sl-mt-14{margin-top:56px}.sm\:sl-mr-14{margin-right:56px}.sm\:sl-mb-14{margin-bottom:56px}.sm\:sl-ml-14{margin-left:56px}.sm\:sl-mt-16{margin-top:64px}.sm\:sl-mr-16{margin-right:64px}.sm\:sl-mb-16{margin-bottom:64px}.sm\:sl-ml-16{margin-left:64px}.sm\:sl-mt-20{margin-top:80px}.sm\:sl-mr-20{margin-right:80px}.sm\:sl-mb-20{margin-bottom:80px}.sm\:sl-ml-20{margin-left:80px}.sm\:sl-mt-24{margin-top:96px}.sm\:sl-mr-24{margin-right:96px}.sm\:sl-mb-24{margin-bottom:96px}.sm\:sl-ml-24{margin-left:96px}.sm\:sl-mt-28{margin-top:112px}.sm\:sl-mr-28{margin-right:112px}.sm\:sl-mb-28{margin-bottom:112px}.sm\:sl-ml-28{margin-left:112px}.sm\:sl-mt-32{margin-top:128px}.sm\:sl-mr-32{margin-right:128px}.sm\:sl-mb-32{margin-bottom:128px}.sm\:sl-ml-32{margin-left:128px}.sm\:sl-mt-36{margin-top:144px}.sm\:sl-mr-36{margin-right:144px}.sm\:sl-mb-36{margin-bottom:144px}.sm\:sl-ml-36{margin-left:144px}.sm\:sl-mt-40{margin-top:160px}.sm\:sl-mr-40{margin-right:160px}.sm\:sl-mb-40{margin-bottom:160px}.sm\:sl-ml-40{margin-left:160px}.sm\:sl-mt-44{margin-top:176px}.sm\:sl-mr-44{margin-right:176px}.sm\:sl-mb-44{margin-bottom:176px}.sm\:sl-ml-44{margin-left:176px}.sm\:sl-mt-48{margin-top:192px}.sm\:sl-mr-48{margin-right:192px}.sm\:sl-mb-48{margin-bottom:192px}.sm\:sl-ml-48{margin-left:192px}.sm\:sl-mt-52{margin-top:208px}.sm\:sl-mr-52{margin-right:208px}.sm\:sl-mb-52{margin-bottom:208px}.sm\:sl-ml-52{margin-left:208px}.sm\:sl-mt-56{margin-top:224px}.sm\:sl-mr-56{margin-right:224px}.sm\:sl-mb-56{margin-bottom:224px}.sm\:sl-ml-56{margin-left:224px}.sm\:sl-mt-60{margin-top:240px}.sm\:sl-mr-60{margin-right:240px}.sm\:sl-mb-60{margin-bottom:240px}.sm\:sl-ml-60{margin-left:240px}.sm\:sl-mt-64{margin-top:256px}.sm\:sl-mr-64{margin-right:256px}.sm\:sl-mb-64{margin-bottom:256px}.sm\:sl-ml-64{margin-left:256px}.sm\:sl-mt-72{margin-top:288px}.sm\:sl-mr-72{margin-right:288px}.sm\:sl-mb-72{margin-bottom:288px}.sm\:sl-ml-72{margin-left:288px}.sm\:sl-mt-80{margin-top:320px}.sm\:sl-mr-80{margin-right:320px}.sm\:sl-mb-80{margin-bottom:320px}.sm\:sl-ml-80{margin-left:320px}.sm\:sl-mt-96{margin-top:384px}.sm\:sl-mr-96{margin-right:384px}.sm\:sl-mb-96{margin-bottom:384px}.sm\:sl-ml-96{margin-left:384px}.sm\:sl-mt-auto{margin-top:auto}.sm\:sl-mr-auto{margin-right:auto}.sm\:sl-mb-auto{margin-bottom:auto}.sm\:sl-ml-auto{margin-left:auto}.sm\:sl-mt-px{margin-top:1px}.sm\:sl-mr-px{margin-right:1px}.sm\:sl-mb-px{margin-bottom:1px}.sm\:sl-ml-px{margin-left:1px}.sm\:sl-mt-0\.5{margin-top:2px}.sm\:sl-mr-0\.5{margin-right:2px}.sm\:sl-mb-0\.5{margin-bottom:2px}.sm\:sl-ml-0\.5{margin-left:2px}.sm\:sl-mt-1\.5{margin-top:6px}.sm\:sl-mr-1\.5{margin-right:6px}.sm\:sl-mb-1\.5{margin-bottom:6px}.sm\:sl-ml-1\.5{margin-left:6px}.sm\:sl-mt-2\.5{margin-top:10px}.sm\:sl-mr-2\.5{margin-right:10px}.sm\:sl-mb-2\.5{margin-bottom:10px}.sm\:sl-ml-2\.5{margin-left:10px}.sm\:sl-mt-3\.5{margin-top:14px}.sm\:sl-mr-3\.5{margin-right:14px}.sm\:sl-mb-3\.5{margin-bottom:14px}.sm\:sl-ml-3\.5{margin-left:14px}.sm\:sl-mt-4\.5{margin-top:18px}.sm\:sl-mr-4\.5{margin-right:18px}.sm\:sl-mb-4\.5{margin-bottom:18px}.sm\:sl-ml-4\.5{margin-left:18px}.sm\:sl--mt-0{margin-top:0}.sm\:sl--mr-0{margin-right:0}.sm\:sl--mb-0{margin-bottom:0}.sm\:sl--ml-0{margin-left:0}.sm\:sl--mt-1{margin-top:-4px}.sm\:sl--mr-1{margin-right:-4px}.sm\:sl--mb-1{margin-bottom:-4px}.sm\:sl--ml-1{margin-left:-4px}.sm\:sl--mt-2{margin-top:-8px}.sm\:sl--mr-2{margin-right:-8px}.sm\:sl--mb-2{margin-bottom:-8px}.sm\:sl--ml-2{margin-left:-8px}.sm\:sl--mt-3{margin-top:-12px}.sm\:sl--mr-3{margin-right:-12px}.sm\:sl--mb-3{margin-bottom:-12px}.sm\:sl--ml-3{margin-left:-12px}.sm\:sl--mt-4{margin-top:-16px}.sm\:sl--mr-4{margin-right:-16px}.sm\:sl--mb-4{margin-bottom:-16px}.sm\:sl--ml-4{margin-left:-16px}.sm\:sl--mt-5{margin-top:-20px}.sm\:sl--mr-5{margin-right:-20px}.sm\:sl--mb-5{margin-bottom:-20px}.sm\:sl--ml-5{margin-left:-20px}.sm\:sl--mt-6{margin-top:-24px}.sm\:sl--mr-6{margin-right:-24px}.sm\:sl--mb-6{margin-bottom:-24px}.sm\:sl--ml-6{margin-left:-24px}.sm\:sl--mt-7{margin-top:-28px}.sm\:sl--mr-7{margin-right:-28px}.sm\:sl--mb-7{margin-bottom:-28px}.sm\:sl--ml-7{margin-left:-28px}.sm\:sl--mt-8{margin-top:-32px}.sm\:sl--mr-8{margin-right:-32px}.sm\:sl--mb-8{margin-bottom:-32px}.sm\:sl--ml-8{margin-left:-32px}.sm\:sl--mt-9{margin-top:-36px}.sm\:sl--mr-9{margin-right:-36px}.sm\:sl--mb-9{margin-bottom:-36px}.sm\:sl--ml-9{margin-left:-36px}.sm\:sl--mt-10{margin-top:-40px}.sm\:sl--mr-10{margin-right:-40px}.sm\:sl--mb-10{margin-bottom:-40px}.sm\:sl--ml-10{margin-left:-40px}.sm\:sl--mt-11{margin-top:-44px}.sm\:sl--mr-11{margin-right:-44px}.sm\:sl--mb-11{margin-bottom:-44px}.sm\:sl--ml-11{margin-left:-44px}.sm\:sl--mt-12{margin-top:-48px}.sm\:sl--mr-12{margin-right:-48px}.sm\:sl--mb-12{margin-bottom:-48px}.sm\:sl--ml-12{margin-left:-48px}.sm\:sl--mt-14{margin-top:-56px}.sm\:sl--mr-14{margin-right:-56px}.sm\:sl--mb-14{margin-bottom:-56px}.sm\:sl--ml-14{margin-left:-56px}.sm\:sl--mt-16{margin-top:-64px}.sm\:sl--mr-16{margin-right:-64px}.sm\:sl--mb-16{margin-bottom:-64px}.sm\:sl--ml-16{margin-left:-64px}.sm\:sl--mt-20{margin-top:-80px}.sm\:sl--mr-20{margin-right:-80px}.sm\:sl--mb-20{margin-bottom:-80px}.sm\:sl--ml-20{margin-left:-80px}.sm\:sl--mt-24{margin-top:-96px}.sm\:sl--mr-24{margin-right:-96px}.sm\:sl--mb-24{margin-bottom:-96px}.sm\:sl--ml-24{margin-left:-96px}.sm\:sl--mt-28{margin-top:-112px}.sm\:sl--mr-28{margin-right:-112px}.sm\:sl--mb-28{margin-bottom:-112px}.sm\:sl--ml-28{margin-left:-112px}.sm\:sl--mt-32{margin-top:-128px}.sm\:sl--mr-32{margin-right:-128px}.sm\:sl--mb-32{margin-bottom:-128px}.sm\:sl--ml-32{margin-left:-128px}.sm\:sl--mt-36{margin-top:-144px}.sm\:sl--mr-36{margin-right:-144px}.sm\:sl--mb-36{margin-bottom:-144px}.sm\:sl--ml-36{margin-left:-144px}.sm\:sl--mt-40{margin-top:-160px}.sm\:sl--mr-40{margin-right:-160px}.sm\:sl--mb-40{margin-bottom:-160px}.sm\:sl--ml-40{margin-left:-160px}.sm\:sl--mt-44{margin-top:-176px}.sm\:sl--mr-44{margin-right:-176px}.sm\:sl--mb-44{margin-bottom:-176px}.sm\:sl--ml-44{margin-left:-176px}.sm\:sl--mt-48{margin-top:-192px}.sm\:sl--mr-48{margin-right:-192px}.sm\:sl--mb-48{margin-bottom:-192px}.sm\:sl--ml-48{margin-left:-192px}.sm\:sl--mt-52{margin-top:-208px}.sm\:sl--mr-52{margin-right:-208px}.sm\:sl--mb-52{margin-bottom:-208px}.sm\:sl--ml-52{margin-left:-208px}.sm\:sl--mt-56{margin-top:-224px}.sm\:sl--mr-56{margin-right:-224px}.sm\:sl--mb-56{margin-bottom:-224px}.sm\:sl--ml-56{margin-left:-224px}.sm\:sl--mt-60{margin-top:-240px}.sm\:sl--mr-60{margin-right:-240px}.sm\:sl--mb-60{margin-bottom:-240px}.sm\:sl--ml-60{margin-left:-240px}.sm\:sl--mt-64{margin-top:-256px}.sm\:sl--mr-64{margin-right:-256px}.sm\:sl--mb-64{margin-bottom:-256px}.sm\:sl--ml-64{margin-left:-256px}.sm\:sl--mt-72{margin-top:-288px}.sm\:sl--mr-72{margin-right:-288px}.sm\:sl--mb-72{margin-bottom:-288px}.sm\:sl--ml-72{margin-left:-288px}.sm\:sl--mt-80{margin-top:-320px}.sm\:sl--mr-80{margin-right:-320px}.sm\:sl--mb-80{margin-bottom:-320px}.sm\:sl--ml-80{margin-left:-320px}.sm\:sl--mt-96{margin-top:-384px}.sm\:sl--mr-96{margin-right:-384px}.sm\:sl--mb-96{margin-bottom:-384px}.sm\:sl--ml-96{margin-left:-384px}.sm\:sl--mt-px{margin-top:-1px}.sm\:sl--mr-px{margin-right:-1px}.sm\:sl--mb-px{margin-bottom:-1px}.sm\:sl--ml-px{margin-left:-1px}.sm\:sl--mt-0\.5{margin-top:-2px}.sm\:sl--mr-0\.5{margin-right:-2px}.sm\:sl--mb-0\.5{margin-bottom:-2px}.sm\:sl--ml-0\.5{margin-left:-2px}.sm\:sl--mt-1\.5{margin-top:-6px}.sm\:sl--mr-1\.5{margin-right:-6px}.sm\:sl--mb-1\.5{margin-bottom:-6px}.sm\:sl--ml-1\.5{margin-left:-6px}.sm\:sl--mt-2\.5{margin-top:-10px}.sm\:sl--mr-2\.5{margin-right:-10px}.sm\:sl--mb-2\.5{margin-bottom:-10px}.sm\:sl--ml-2\.5{margin-left:-10px}.sm\:sl--mt-3\.5{margin-top:-14px}.sm\:sl--mr-3\.5{margin-right:-14px}.sm\:sl--mb-3\.5{margin-bottom:-14px}.sm\:sl--ml-3\.5{margin-left:-14px}.sm\:sl--mt-4\.5{margin-top:-18px}.sm\:sl--mr-4\.5{margin-right:-18px}.sm\:sl--mb-4\.5{margin-bottom:-18px}.sm\:sl--ml-4\.5{margin-left:-18px}.sm\:sl-max-h-full{max-height:100%}.sm\:sl-max-h-screen{max-height:100vh}.sm\:sl-max-w-none{max-width:none}.sm\:sl-max-w-full{max-width:100%}.sm\:sl-max-w-min{max-width:-moz-min-content;max-width:min-content}.sm\:sl-max-w-max{max-width:-moz-max-content;max-width:max-content}.sm\:sl-max-w-prose{max-width:65ch}.sm\:sl-min-h-full{min-height:100%}.sm\:sl-min-h-screen{min-height:100vh}.sm\:sl-min-w-full{min-width:100%}.sm\:sl-min-w-min{min-width:-moz-min-content;min-width:min-content}.sm\:sl-min-w-max{min-width:-moz-max-content;min-width:max-content}.sm\:sl-p-0{padding:0}.sm\:sl-p-1{padding:4px}.sm\:sl-p-2{padding:8px}.sm\:sl-p-3{padding:12px}.sm\:sl-p-4{padding:16px}.sm\:sl-p-5{padding:20px}.sm\:sl-p-6{padding:24px}.sm\:sl-p-7{padding:28px}.sm\:sl-p-8{padding:32px}.sm\:sl-p-9{padding:36px}.sm\:sl-p-10{padding:40px}.sm\:sl-p-11{padding:44px}.sm\:sl-p-12{padding:48px}.sm\:sl-p-14{padding:56px}.sm\:sl-p-16{padding:64px}.sm\:sl-p-20{padding:80px}.sm\:sl-p-24{padding:96px}.sm\:sl-p-28{padding:112px}.sm\:sl-p-32{padding:128px}.sm\:sl-p-36{padding:144px}.sm\:sl-p-40{padding:160px}.sm\:sl-p-44{padding:176px}.sm\:sl-p-48{padding:192px}.sm\:sl-p-52{padding:208px}.sm\:sl-p-56{padding:224px}.sm\:sl-p-60{padding:240px}.sm\:sl-p-64{padding:256px}.sm\:sl-p-72{padding:288px}.sm\:sl-p-80{padding:320px}.sm\:sl-p-96{padding:384px}.sm\:sl-p-px{padding:1px}.sm\:sl-p-0\.5{padding:2px}.sm\:sl-p-1\.5{padding:6px}.sm\:sl-p-2\.5{padding:10px}.sm\:sl-p-3\.5{padding:14px}.sm\:sl-p-4\.5{padding:18px}.sm\:sl-py-0{padding-bottom:0;padding-top:0}.sm\:sl-px-0{padding-left:0;padding-right:0}.sm\:sl-py-1{padding-bottom:4px;padding-top:4px}.sm\:sl-px-1{padding-left:4px;padding-right:4px}.sm\:sl-py-2{padding-bottom:8px;padding-top:8px}.sm\:sl-px-2{padding-left:8px;padding-right:8px}.sm\:sl-py-3{padding-bottom:12px;padding-top:12px}.sm\:sl-px-3{padding-left:12px;padding-right:12px}.sm\:sl-py-4{padding-bottom:16px;padding-top:16px}.sm\:sl-px-4{padding-left:16px;padding-right:16px}.sm\:sl-py-5{padding-bottom:20px;padding-top:20px}.sm\:sl-px-5{padding-left:20px;padding-right:20px}.sm\:sl-py-6{padding-bottom:24px;padding-top:24px}.sm\:sl-px-6{padding-left:24px;padding-right:24px}.sm\:sl-py-7{padding-bottom:28px;padding-top:28px}.sm\:sl-px-7{padding-left:28px;padding-right:28px}.sm\:sl-py-8{padding-bottom:32px;padding-top:32px}.sm\:sl-px-8{padding-left:32px;padding-right:32px}.sm\:sl-py-9{padding-bottom:36px;padding-top:36px}.sm\:sl-px-9{padding-left:36px;padding-right:36px}.sm\:sl-py-10{padding-bottom:40px;padding-top:40px}.sm\:sl-px-10{padding-left:40px;padding-right:40px}.sm\:sl-py-11{padding-bottom:44px;padding-top:44px}.sm\:sl-px-11{padding-left:44px;padding-right:44px}.sm\:sl-py-12{padding-bottom:48px;padding-top:48px}.sm\:sl-px-12{padding-left:48px;padding-right:48px}.sm\:sl-py-14{padding-bottom:56px;padding-top:56px}.sm\:sl-px-14{padding-left:56px;padding-right:56px}.sm\:sl-py-16{padding-bottom:64px;padding-top:64px}.sm\:sl-px-16{padding-left:64px;padding-right:64px}.sm\:sl-py-20{padding-bottom:80px;padding-top:80px}.sm\:sl-px-20{padding-left:80px;padding-right:80px}.sm\:sl-py-24{padding-bottom:96px;padding-top:96px}.sm\:sl-px-24{padding-left:96px;padding-right:96px}.sm\:sl-py-28{padding-bottom:112px;padding-top:112px}.sm\:sl-px-28{padding-left:112px;padding-right:112px}.sm\:sl-py-32{padding-bottom:128px;padding-top:128px}.sm\:sl-px-32{padding-left:128px;padding-right:128px}.sm\:sl-py-36{padding-bottom:144px;padding-top:144px}.sm\:sl-px-36{padding-left:144px;padding-right:144px}.sm\:sl-py-40{padding-bottom:160px;padding-top:160px}.sm\:sl-px-40{padding-left:160px;padding-right:160px}.sm\:sl-py-44{padding-bottom:176px;padding-top:176px}.sm\:sl-px-44{padding-left:176px;padding-right:176px}.sm\:sl-py-48{padding-bottom:192px;padding-top:192px}.sm\:sl-px-48{padding-left:192px;padding-right:192px}.sm\:sl-py-52{padding-bottom:208px;padding-top:208px}.sm\:sl-px-52{padding-left:208px;padding-right:208px}.sm\:sl-py-56{padding-bottom:224px;padding-top:224px}.sm\:sl-px-56{padding-left:224px;padding-right:224px}.sm\:sl-py-60{padding-bottom:240px;padding-top:240px}.sm\:sl-px-60{padding-left:240px;padding-right:240px}.sm\:sl-py-64{padding-bottom:256px;padding-top:256px}.sm\:sl-px-64{padding-left:256px;padding-right:256px}.sm\:sl-py-72{padding-bottom:288px;padding-top:288px}.sm\:sl-px-72{padding-left:288px;padding-right:288px}.sm\:sl-py-80{padding-bottom:320px;padding-top:320px}.sm\:sl-px-80{padding-left:320px;padding-right:320px}.sm\:sl-py-96{padding-bottom:384px;padding-top:384px}.sm\:sl-px-96{padding-left:384px;padding-right:384px}.sm\:sl-py-px{padding-bottom:1px;padding-top:1px}.sm\:sl-px-px{padding-left:1px;padding-right:1px}.sm\:sl-py-0\.5{padding-bottom:2px;padding-top:2px}.sm\:sl-px-0\.5{padding-left:2px;padding-right:2px}.sm\:sl-py-1\.5{padding-bottom:6px;padding-top:6px}.sm\:sl-px-1\.5{padding-left:6px;padding-right:6px}.sm\:sl-py-2\.5{padding-bottom:10px;padding-top:10px}.sm\:sl-px-2\.5{padding-left:10px;padding-right:10px}.sm\:sl-py-3\.5{padding-bottom:14px;padding-top:14px}.sm\:sl-px-3\.5{padding-left:14px;padding-right:14px}.sm\:sl-py-4\.5{padding-bottom:18px;padding-top:18px}.sm\:sl-px-4\.5{padding-left:18px;padding-right:18px}.sm\:sl-pt-0{padding-top:0}.sm\:sl-pr-0{padding-right:0}.sm\:sl-pb-0{padding-bottom:0}.sm\:sl-pl-0{padding-left:0}.sm\:sl-pt-1{padding-top:4px}.sm\:sl-pr-1{padding-right:4px}.sm\:sl-pb-1{padding-bottom:4px}.sm\:sl-pl-1{padding-left:4px}.sm\:sl-pt-2{padding-top:8px}.sm\:sl-pr-2{padding-right:8px}.sm\:sl-pb-2{padding-bottom:8px}.sm\:sl-pl-2{padding-left:8px}.sm\:sl-pt-3{padding-top:12px}.sm\:sl-pr-3{padding-right:12px}.sm\:sl-pb-3{padding-bottom:12px}.sm\:sl-pl-3{padding-left:12px}.sm\:sl-pt-4{padding-top:16px}.sm\:sl-pr-4{padding-right:16px}.sm\:sl-pb-4{padding-bottom:16px}.sm\:sl-pl-4{padding-left:16px}.sm\:sl-pt-5{padding-top:20px}.sm\:sl-pr-5{padding-right:20px}.sm\:sl-pb-5{padding-bottom:20px}.sm\:sl-pl-5{padding-left:20px}.sm\:sl-pt-6{padding-top:24px}.sm\:sl-pr-6{padding-right:24px}.sm\:sl-pb-6{padding-bottom:24px}.sm\:sl-pl-6{padding-left:24px}.sm\:sl-pt-7{padding-top:28px}.sm\:sl-pr-7{padding-right:28px}.sm\:sl-pb-7{padding-bottom:28px}.sm\:sl-pl-7{padding-left:28px}.sm\:sl-pt-8{padding-top:32px}.sm\:sl-pr-8{padding-right:32px}.sm\:sl-pb-8{padding-bottom:32px}.sm\:sl-pl-8{padding-left:32px}.sm\:sl-pt-9{padding-top:36px}.sm\:sl-pr-9{padding-right:36px}.sm\:sl-pb-9{padding-bottom:36px}.sm\:sl-pl-9{padding-left:36px}.sm\:sl-pt-10{padding-top:40px}.sm\:sl-pr-10{padding-right:40px}.sm\:sl-pb-10{padding-bottom:40px}.sm\:sl-pl-10{padding-left:40px}.sm\:sl-pt-11{padding-top:44px}.sm\:sl-pr-11{padding-right:44px}.sm\:sl-pb-11{padding-bottom:44px}.sm\:sl-pl-11{padding-left:44px}.sm\:sl-pt-12{padding-top:48px}.sm\:sl-pr-12{padding-right:48px}.sm\:sl-pb-12{padding-bottom:48px}.sm\:sl-pl-12{padding-left:48px}.sm\:sl-pt-14{padding-top:56px}.sm\:sl-pr-14{padding-right:56px}.sm\:sl-pb-14{padding-bottom:56px}.sm\:sl-pl-14{padding-left:56px}.sm\:sl-pt-16{padding-top:64px}.sm\:sl-pr-16{padding-right:64px}.sm\:sl-pb-16{padding-bottom:64px}.sm\:sl-pl-16{padding-left:64px}.sm\:sl-pt-20{padding-top:80px}.sm\:sl-pr-20{padding-right:80px}.sm\:sl-pb-20{padding-bottom:80px}.sm\:sl-pl-20{padding-left:80px}.sm\:sl-pt-24{padding-top:96px}.sm\:sl-pr-24{padding-right:96px}.sm\:sl-pb-24{padding-bottom:96px}.sm\:sl-pl-24{padding-left:96px}.sm\:sl-pt-28{padding-top:112px}.sm\:sl-pr-28{padding-right:112px}.sm\:sl-pb-28{padding-bottom:112px}.sm\:sl-pl-28{padding-left:112px}.sm\:sl-pt-32{padding-top:128px}.sm\:sl-pr-32{padding-right:128px}.sm\:sl-pb-32{padding-bottom:128px}.sm\:sl-pl-32{padding-left:128px}.sm\:sl-pt-36{padding-top:144px}.sm\:sl-pr-36{padding-right:144px}.sm\:sl-pb-36{padding-bottom:144px}.sm\:sl-pl-36{padding-left:144px}.sm\:sl-pt-40{padding-top:160px}.sm\:sl-pr-40{padding-right:160px}.sm\:sl-pb-40{padding-bottom:160px}.sm\:sl-pl-40{padding-left:160px}.sm\:sl-pt-44{padding-top:176px}.sm\:sl-pr-44{padding-right:176px}.sm\:sl-pb-44{padding-bottom:176px}.sm\:sl-pl-44{padding-left:176px}.sm\:sl-pt-48{padding-top:192px}.sm\:sl-pr-48{padding-right:192px}.sm\:sl-pb-48{padding-bottom:192px}.sm\:sl-pl-48{padding-left:192px}.sm\:sl-pt-52{padding-top:208px}.sm\:sl-pr-52{padding-right:208px}.sm\:sl-pb-52{padding-bottom:208px}.sm\:sl-pl-52{padding-left:208px}.sm\:sl-pt-56{padding-top:224px}.sm\:sl-pr-56{padding-right:224px}.sm\:sl-pb-56{padding-bottom:224px}.sm\:sl-pl-56{padding-left:224px}.sm\:sl-pt-60{padding-top:240px}.sm\:sl-pr-60{padding-right:240px}.sm\:sl-pb-60{padding-bottom:240px}.sm\:sl-pl-60{padding-left:240px}.sm\:sl-pt-64{padding-top:256px}.sm\:sl-pr-64{padding-right:256px}.sm\:sl-pb-64{padding-bottom:256px}.sm\:sl-pl-64{padding-left:256px}.sm\:sl-pt-72{padding-top:288px}.sm\:sl-pr-72{padding-right:288px}.sm\:sl-pb-72{padding-bottom:288px}.sm\:sl-pl-72{padding-left:288px}.sm\:sl-pt-80{padding-top:320px}.sm\:sl-pr-80{padding-right:320px}.sm\:sl-pb-80{padding-bottom:320px}.sm\:sl-pl-80{padding-left:320px}.sm\:sl-pt-96{padding-top:384px}.sm\:sl-pr-96{padding-right:384px}.sm\:sl-pb-96{padding-bottom:384px}.sm\:sl-pl-96{padding-left:384px}.sm\:sl-pt-px{padding-top:1px}.sm\:sl-pr-px{padding-right:1px}.sm\:sl-pb-px{padding-bottom:1px}.sm\:sl-pl-px{padding-left:1px}.sm\:sl-pt-0\.5{padding-top:2px}.sm\:sl-pr-0\.5{padding-right:2px}.sm\:sl-pb-0\.5{padding-bottom:2px}.sm\:sl-pl-0\.5{padding-left:2px}.sm\:sl-pt-1\.5{padding-top:6px}.sm\:sl-pr-1\.5{padding-right:6px}.sm\:sl-pb-1\.5{padding-bottom:6px}.sm\:sl-pl-1\.5{padding-left:6px}.sm\:sl-pt-2\.5{padding-top:10px}.sm\:sl-pr-2\.5{padding-right:10px}.sm\:sl-pb-2\.5{padding-bottom:10px}.sm\:sl-pl-2\.5{padding-left:10px}.sm\:sl-pt-3\.5{padding-top:14px}.sm\:sl-pr-3\.5{padding-right:14px}.sm\:sl-pb-3\.5{padding-bottom:14px}.sm\:sl-pl-3\.5{padding-left:14px}.sm\:sl-pt-4\.5{padding-top:18px}.sm\:sl-pr-4\.5{padding-right:18px}.sm\:sl-pb-4\.5{padding-bottom:18px}.sm\:sl-pl-4\.5{padding-left:18px}.sm\:sl-static{position:static}.sm\:sl-fixed{position:fixed}.sm\:sl-absolute{position:absolute}.sm\:sl-relative{position:relative}.sm\:sl-sticky{position:-webkit-sticky;position:sticky}.sm\:sl-visible{visibility:visible}.sm\:sl-invisible{visibility:hidden}.sl-group:hover .sm\:group-hover\:sl-visible{visibility:visible}.sl-group:hover .sm\:group-hover\:sl-invisible{visibility:hidden}.sl-group:focus .sm\:group-focus\:sl-visible{visibility:visible}.sl-group:focus .sm\:group-focus\:sl-invisible{visibility:hidden}.sm\:sl-w-0{width:0}.sm\:sl-w-1{width:4px}.sm\:sl-w-2{width:8px}.sm\:sl-w-3{width:12px}.sm\:sl-w-4{width:16px}.sm\:sl-w-5{width:20px}.sm\:sl-w-6{width:24px}.sm\:sl-w-7{width:28px}.sm\:sl-w-8{width:32px}.sm\:sl-w-9{width:36px}.sm\:sl-w-10{width:40px}.sm\:sl-w-11{width:44px}.sm\:sl-w-12{width:48px}.sm\:sl-w-14{width:56px}.sm\:sl-w-16{width:64px}.sm\:sl-w-20{width:80px}.sm\:sl-w-24{width:96px}.sm\:sl-w-28{width:112px}.sm\:sl-w-32{width:128px}.sm\:sl-w-36{width:144px}.sm\:sl-w-40{width:160px}.sm\:sl-w-44{width:176px}.sm\:sl-w-48{width:192px}.sm\:sl-w-52{width:208px}.sm\:sl-w-56{width:224px}.sm\:sl-w-60{width:240px}.sm\:sl-w-64{width:256px}.sm\:sl-w-72{width:288px}.sm\:sl-w-80{width:320px}.sm\:sl-w-96{width:384px}.sm\:sl-w-auto{width:auto}.sm\:sl-w-px{width:1px}.sm\:sl-w-0\.5{width:2px}.sm\:sl-w-1\.5{width:6px}.sm\:sl-w-2\.5{width:10px}.sm\:sl-w-3\.5{width:14px}.sm\:sl-w-4\.5{width:18px}.sm\:sl-w-xs{width:20px}.sm\:sl-w-sm{width:24px}.sm\:sl-w-md{width:32px}.sm\:sl-w-lg{width:36px}.sm\:sl-w-xl{width:44px}.sm\:sl-w-2xl{width:52px}.sm\:sl-w-3xl{width:60px}.sm\:sl-w-1\/2{width:50%}.sm\:sl-w-1\/3{width:33.333333%}.sm\:sl-w-2\/3{width:66.666667%}.sm\:sl-w-1\/4{width:25%}.sm\:sl-w-2\/4{width:50%}.sm\:sl-w-3\/4{width:75%}.sm\:sl-w-1\/5{width:20%}.sm\:sl-w-2\/5{width:40%}.sm\:sl-w-3\/5{width:60%}.sm\:sl-w-4\/5{width:80%}.sm\:sl-w-1\/6{width:16.666667%}.sm\:sl-w-2\/6{width:33.333333%}.sm\:sl-w-3\/6{width:50%}.sm\:sl-w-4\/6{width:66.666667%}.sm\:sl-w-5\/6{width:83.333333%}.sm\:sl-w-full{width:100%}.sm\:sl-w-screen{width:100vw}.sm\:sl-w-min{width:-moz-min-content;width:min-content}.sm\:sl-w-max{width:-moz-max-content;width:max-content}}@media (max-width:767px){.md\:sl-stack--1{gap:4px}.md\:sl-stack--2{gap:8px}.md\:sl-stack--3{gap:12px}.md\:sl-stack--4{gap:16px}.md\:sl-stack--5{gap:20px}.md\:sl-stack--6{gap:24px}.md\:sl-stack--7{gap:28px}.md\:sl-stack--8{gap:32px}.md\:sl-stack--9{gap:36px}.md\:sl-stack--10{gap:40px}.md\:sl-stack--12{gap:48px}.md\:sl-stack--14{gap:56px}.md\:sl-stack--16{gap:64px}.md\:sl-stack--20{gap:80px}.md\:sl-stack--24{gap:96px}.md\:sl-stack--32{gap:128px}.md\:sl-content-center{align-content:center}.md\:sl-content-start{align-content:flex-start}.md\:sl-content-end{align-content:flex-end}.md\:sl-content-between{align-content:space-between}.md\:sl-content-around{align-content:space-around}.md\:sl-content-evenly{align-content:space-evenly}.md\:sl-items-start{align-items:flex-start}.md\:sl-items-end{align-items:flex-end}.md\:sl-items-center{align-items:center}.md\:sl-items-baseline{align-items:baseline}.md\:sl-items-stretch{align-items:stretch}.md\:sl-self-auto{align-self:auto}.md\:sl-self-start{align-self:flex-start}.md\:sl-self-end{align-self:flex-end}.md\:sl-self-center{align-self:center}.md\:sl-self-stretch{align-self:stretch}.md\:sl-blur-0,.md\:sl-blur-none{--tw-blur:blur(0)}.md\:sl-blur-sm{--tw-blur:blur(4px)}.md\:sl-blur{--tw-blur:blur(8px)}.md\:sl-blur-md{--tw-blur:blur(12px)}.md\:sl-blur-lg{--tw-blur:blur(16px)}.md\:sl-blur-xl{--tw-blur:blur(24px)}.md\:sl-blur-2xl{--tw-blur:blur(40px)}.md\:sl-blur-3xl{--tw-blur:blur(64px)}.md\:sl-block{display:block}.md\:sl-inline-block{display:inline-block}.md\:sl-inline{display:inline}.md\:sl-flex{display:flex}.md\:sl-inline-flex{display:inline-flex}.md\:sl-table{display:table}.md\:sl-inline-table{display:inline-table}.md\:sl-table-caption{display:table-caption}.md\:sl-table-cell{display:table-cell}.md\:sl-table-column{display:table-column}.md\:sl-table-column-group{display:table-column-group}.md\:sl-table-footer-group{display:table-footer-group}.md\:sl-table-header-group{display:table-header-group}.md\:sl-table-row-group{display:table-row-group}.md\:sl-table-row{display:table-row}.md\:sl-flow-root{display:flow-root}.md\:sl-grid{display:grid}.md\:sl-inline-grid{display:inline-grid}.md\:sl-contents{display:contents}.md\:sl-list-item{display:list-item}.md\:sl-hidden{display:none}.md\:sl-drop-shadow{--tw-drop-shadow:drop-shadow(var(--drop-shadow-default1)) drop-shadow(var(--drop-shadow-default2))}.md\:sl-flex-1{flex:1 1}.md\:sl-flex-auto{flex:1 1 auto}.md\:sl-flex-initial{flex:0 1 auto}.md\:sl-flex-none{flex:none}.md\:sl-flex-row{flex-direction:row}.md\:sl-flex-row-reverse{flex-direction:row-reverse}.md\:sl-flex-col{flex-direction:column}.md\:sl-flex-col-reverse{flex-direction:column-reverse}.md\:sl-flex-grow-0{flex-grow:0}.md\:sl-flex-grow{flex-grow:1}.md\:sl-flex-shrink-0{flex-shrink:0}.md\:sl-flex-shrink{flex-shrink:1}.md\:sl-flex-wrap{flex-wrap:wrap}.md\:sl-flex-wrap-reverse{flex-wrap:wrap-reverse}.md\:sl-flex-nowrap{flex-wrap:nowrap}.md\:sl-h-0{height:0}.md\:sl-h-1{height:4px}.md\:sl-h-2{height:8px}.md\:sl-h-3{height:12px}.md\:sl-h-4{height:16px}.md\:sl-h-5{height:20px}.md\:sl-h-6{height:24px}.md\:sl-h-7{height:28px}.md\:sl-h-8{height:32px}.md\:sl-h-9{height:36px}.md\:sl-h-10{height:40px}.md\:sl-h-11{height:44px}.md\:sl-h-12{height:48px}.md\:sl-h-14{height:56px}.md\:sl-h-16{height:64px}.md\:sl-h-20{height:80px}.md\:sl-h-24{height:96px}.md\:sl-h-28{height:112px}.md\:sl-h-32{height:128px}.md\:sl-h-36{height:144px}.md\:sl-h-40{height:160px}.md\:sl-h-44{height:176px}.md\:sl-h-48{height:192px}.md\:sl-h-52{height:208px}.md\:sl-h-56{height:224px}.md\:sl-h-60{height:240px}.md\:sl-h-64{height:256px}.md\:sl-h-72{height:288px}.md\:sl-h-80{height:320px}.md\:sl-h-96{height:384px}.md\:sl-h-auto{height:auto}.md\:sl-h-px{height:1px}.md\:sl-h-0\.5{height:2px}.md\:sl-h-1\.5{height:6px}.md\:sl-h-2\.5{height:10px}.md\:sl-h-3\.5{height:14px}.md\:sl-h-4\.5{height:18px}.md\:sl-h-xs{height:20px}.md\:sl-h-sm{height:24px}.md\:sl-h-md{height:32px}.md\:sl-h-lg{height:36px}.md\:sl-h-xl{height:44px}.md\:sl-h-2xl{height:52px}.md\:sl-h-3xl{height:60px}.md\:sl-h-full{height:100%}.md\:sl-h-screen{height:100vh}.md\:sl-justify-start{justify-content:flex-start}.md\:sl-justify-end{justify-content:flex-end}.md\:sl-justify-center{justify-content:center}.md\:sl-justify-between{justify-content:space-between}.md\:sl-justify-around{justify-content:space-around}.md\:sl-justify-evenly{justify-content:space-evenly}.md\:sl-justify-items-start{justify-items:start}.md\:sl-justify-items-end{justify-items:end}.md\:sl-justify-items-center{justify-items:center}.md\:sl-justify-items-stretch{justify-items:stretch}.md\:sl-justify-self-auto{justify-self:auto}.md\:sl-justify-self-start{justify-self:start}.md\:sl-justify-self-end{justify-self:end}.md\:sl-justify-self-center{justify-self:center}.md\:sl-justify-self-stretch{justify-self:stretch}.md\:sl-m-0{margin:0}.md\:sl-m-1{margin:4px}.md\:sl-m-2{margin:8px}.md\:sl-m-3{margin:12px}.md\:sl-m-4{margin:16px}.md\:sl-m-5{margin:20px}.md\:sl-m-6{margin:24px}.md\:sl-m-7{margin:28px}.md\:sl-m-8{margin:32px}.md\:sl-m-9{margin:36px}.md\:sl-m-10{margin:40px}.md\:sl-m-11{margin:44px}.md\:sl-m-12{margin:48px}.md\:sl-m-14{margin:56px}.md\:sl-m-16{margin:64px}.md\:sl-m-20{margin:80px}.md\:sl-m-24{margin:96px}.md\:sl-m-28{margin:112px}.md\:sl-m-32{margin:128px}.md\:sl-m-36{margin:144px}.md\:sl-m-40{margin:160px}.md\:sl-m-44{margin:176px}.md\:sl-m-48{margin:192px}.md\:sl-m-52{margin:208px}.md\:sl-m-56{margin:224px}.md\:sl-m-60{margin:240px}.md\:sl-m-64{margin:256px}.md\:sl-m-72{margin:288px}.md\:sl-m-80{margin:320px}.md\:sl-m-96{margin:384px}.md\:sl-m-auto{margin:auto}.md\:sl-m-px{margin:1px}.md\:sl-m-0\.5{margin:2px}.md\:sl-m-1\.5{margin:6px}.md\:sl-m-2\.5{margin:10px}.md\:sl-m-3\.5{margin:14px}.md\:sl-m-4\.5{margin:18px}.md\:sl--m-0{margin:0}.md\:sl--m-1{margin:-4px}.md\:sl--m-2{margin:-8px}.md\:sl--m-3{margin:-12px}.md\:sl--m-4{margin:-16px}.md\:sl--m-5{margin:-20px}.md\:sl--m-6{margin:-24px}.md\:sl--m-7{margin:-28px}.md\:sl--m-8{margin:-32px}.md\:sl--m-9{margin:-36px}.md\:sl--m-10{margin:-40px}.md\:sl--m-11{margin:-44px}.md\:sl--m-12{margin:-48px}.md\:sl--m-14{margin:-56px}.md\:sl--m-16{margin:-64px}.md\:sl--m-20{margin:-80px}.md\:sl--m-24{margin:-96px}.md\:sl--m-28{margin:-112px}.md\:sl--m-32{margin:-128px}.md\:sl--m-36{margin:-144px}.md\:sl--m-40{margin:-160px}.md\:sl--m-44{margin:-176px}.md\:sl--m-48{margin:-192px}.md\:sl--m-52{margin:-208px}.md\:sl--m-56{margin:-224px}.md\:sl--m-60{margin:-240px}.md\:sl--m-64{margin:-256px}.md\:sl--m-72{margin:-288px}.md\:sl--m-80{margin:-320px}.md\:sl--m-96{margin:-384px}.md\:sl--m-px{margin:-1px}.md\:sl--m-0\.5{margin:-2px}.md\:sl--m-1\.5{margin:-6px}.md\:sl--m-2\.5{margin:-10px}.md\:sl--m-3\.5{margin:-14px}.md\:sl--m-4\.5{margin:-18px}.md\:sl-my-0{margin-bottom:0;margin-top:0}.md\:sl-mx-0{margin-left:0;margin-right:0}.md\:sl-my-1{margin-bottom:4px;margin-top:4px}.md\:sl-mx-1{margin-left:4px;margin-right:4px}.md\:sl-my-2{margin-bottom:8px;margin-top:8px}.md\:sl-mx-2{margin-left:8px;margin-right:8px}.md\:sl-my-3{margin-bottom:12px;margin-top:12px}.md\:sl-mx-3{margin-left:12px;margin-right:12px}.md\:sl-my-4{margin-bottom:16px;margin-top:16px}.md\:sl-mx-4{margin-left:16px;margin-right:16px}.md\:sl-my-5{margin-bottom:20px;margin-top:20px}.md\:sl-mx-5{margin-left:20px;margin-right:20px}.md\:sl-my-6{margin-bottom:24px;margin-top:24px}.md\:sl-mx-6{margin-left:24px;margin-right:24px}.md\:sl-my-7{margin-bottom:28px;margin-top:28px}.md\:sl-mx-7{margin-left:28px;margin-right:28px}.md\:sl-my-8{margin-bottom:32px;margin-top:32px}.md\:sl-mx-8{margin-left:32px;margin-right:32px}.md\:sl-my-9{margin-bottom:36px;margin-top:36px}.md\:sl-mx-9{margin-left:36px;margin-right:36px}.md\:sl-my-10{margin-bottom:40px;margin-top:40px}.md\:sl-mx-10{margin-left:40px;margin-right:40px}.md\:sl-my-11{margin-bottom:44px;margin-top:44px}.md\:sl-mx-11{margin-left:44px;margin-right:44px}.md\:sl-my-12{margin-bottom:48px;margin-top:48px}.md\:sl-mx-12{margin-left:48px;margin-right:48px}.md\:sl-my-14{margin-bottom:56px;margin-top:56px}.md\:sl-mx-14{margin-left:56px;margin-right:56px}.md\:sl-my-16{margin-bottom:64px;margin-top:64px}.md\:sl-mx-16{margin-left:64px;margin-right:64px}.md\:sl-my-20{margin-bottom:80px;margin-top:80px}.md\:sl-mx-20{margin-left:80px;margin-right:80px}.md\:sl-my-24{margin-bottom:96px;margin-top:96px}.md\:sl-mx-24{margin-left:96px;margin-right:96px}.md\:sl-my-28{margin-bottom:112px;margin-top:112px}.md\:sl-mx-28{margin-left:112px;margin-right:112px}.md\:sl-my-32{margin-bottom:128px;margin-top:128px}.md\:sl-mx-32{margin-left:128px;margin-right:128px}.md\:sl-my-36{margin-bottom:144px;margin-top:144px}.md\:sl-mx-36{margin-left:144px;margin-right:144px}.md\:sl-my-40{margin-bottom:160px;margin-top:160px}.md\:sl-mx-40{margin-left:160px;margin-right:160px}.md\:sl-my-44{margin-bottom:176px;margin-top:176px}.md\:sl-mx-44{margin-left:176px;margin-right:176px}.md\:sl-my-48{margin-bottom:192px;margin-top:192px}.md\:sl-mx-48{margin-left:192px;margin-right:192px}.md\:sl-my-52{margin-bottom:208px;margin-top:208px}.md\:sl-mx-52{margin-left:208px;margin-right:208px}.md\:sl-my-56{margin-bottom:224px;margin-top:224px}.md\:sl-mx-56{margin-left:224px;margin-right:224px}.md\:sl-my-60{margin-bottom:240px;margin-top:240px}.md\:sl-mx-60{margin-left:240px;margin-right:240px}.md\:sl-my-64{margin-bottom:256px;margin-top:256px}.md\:sl-mx-64{margin-left:256px;margin-right:256px}.md\:sl-my-72{margin-bottom:288px;margin-top:288px}.md\:sl-mx-72{margin-left:288px;margin-right:288px}.md\:sl-my-80{margin-bottom:320px;margin-top:320px}.md\:sl-mx-80{margin-left:320px;margin-right:320px}.md\:sl-my-96{margin-bottom:384px;margin-top:384px}.md\:sl-mx-96{margin-left:384px;margin-right:384px}.md\:sl-my-auto{margin-bottom:auto;margin-top:auto}.md\:sl-mx-auto{margin-left:auto;margin-right:auto}.md\:sl-my-px{margin-bottom:1px;margin-top:1px}.md\:sl-mx-px{margin-left:1px;margin-right:1px}.md\:sl-my-0\.5{margin-bottom:2px;margin-top:2px}.md\:sl-mx-0\.5{margin-left:2px;margin-right:2px}.md\:sl-my-1\.5{margin-bottom:6px;margin-top:6px}.md\:sl-mx-1\.5{margin-left:6px;margin-right:6px}.md\:sl-my-2\.5{margin-bottom:10px;margin-top:10px}.md\:sl-mx-2\.5{margin-left:10px;margin-right:10px}.md\:sl-my-3\.5{margin-bottom:14px;margin-top:14px}.md\:sl-mx-3\.5{margin-left:14px;margin-right:14px}.md\:sl-my-4\.5{margin-bottom:18px;margin-top:18px}.md\:sl-mx-4\.5{margin-left:18px;margin-right:18px}.md\:sl--my-0{margin-bottom:0;margin-top:0}.md\:sl--mx-0{margin-left:0;margin-right:0}.md\:sl--my-1{margin-bottom:-4px;margin-top:-4px}.md\:sl--mx-1{margin-left:-4px;margin-right:-4px}.md\:sl--my-2{margin-bottom:-8px;margin-top:-8px}.md\:sl--mx-2{margin-left:-8px;margin-right:-8px}.md\:sl--my-3{margin-bottom:-12px;margin-top:-12px}.md\:sl--mx-3{margin-left:-12px;margin-right:-12px}.md\:sl--my-4{margin-bottom:-16px;margin-top:-16px}.md\:sl--mx-4{margin-left:-16px;margin-right:-16px}.md\:sl--my-5{margin-bottom:-20px;margin-top:-20px}.md\:sl--mx-5{margin-left:-20px;margin-right:-20px}.md\:sl--my-6{margin-bottom:-24px;margin-top:-24px}.md\:sl--mx-6{margin-left:-24px;margin-right:-24px}.md\:sl--my-7{margin-bottom:-28px;margin-top:-28px}.md\:sl--mx-7{margin-left:-28px;margin-right:-28px}.md\:sl--my-8{margin-bottom:-32px;margin-top:-32px}.md\:sl--mx-8{margin-left:-32px;margin-right:-32px}.md\:sl--my-9{margin-bottom:-36px;margin-top:-36px}.md\:sl--mx-9{margin-left:-36px;margin-right:-36px}.md\:sl--my-10{margin-bottom:-40px;margin-top:-40px}.md\:sl--mx-10{margin-left:-40px;margin-right:-40px}.md\:sl--my-11{margin-bottom:-44px;margin-top:-44px}.md\:sl--mx-11{margin-left:-44px;margin-right:-44px}.md\:sl--my-12{margin-bottom:-48px;margin-top:-48px}.md\:sl--mx-12{margin-left:-48px;margin-right:-48px}.md\:sl--my-14{margin-bottom:-56px;margin-top:-56px}.md\:sl--mx-14{margin-left:-56px;margin-right:-56px}.md\:sl--my-16{margin-bottom:-64px;margin-top:-64px}.md\:sl--mx-16{margin-left:-64px;margin-right:-64px}.md\:sl--my-20{margin-bottom:-80px;margin-top:-80px}.md\:sl--mx-20{margin-left:-80px;margin-right:-80px}.md\:sl--my-24{margin-bottom:-96px;margin-top:-96px}.md\:sl--mx-24{margin-left:-96px;margin-right:-96px}.md\:sl--my-28{margin-bottom:-112px;margin-top:-112px}.md\:sl--mx-28{margin-left:-112px;margin-right:-112px}.md\:sl--my-32{margin-bottom:-128px;margin-top:-128px}.md\:sl--mx-32{margin-left:-128px;margin-right:-128px}.md\:sl--my-36{margin-bottom:-144px;margin-top:-144px}.md\:sl--mx-36{margin-left:-144px;margin-right:-144px}.md\:sl--my-40{margin-bottom:-160px;margin-top:-160px}.md\:sl--mx-40{margin-left:-160px;margin-right:-160px}.md\:sl--my-44{margin-bottom:-176px;margin-top:-176px}.md\:sl--mx-44{margin-left:-176px;margin-right:-176px}.md\:sl--my-48{margin-bottom:-192px;margin-top:-192px}.md\:sl--mx-48{margin-left:-192px;margin-right:-192px}.md\:sl--my-52{margin-bottom:-208px;margin-top:-208px}.md\:sl--mx-52{margin-left:-208px;margin-right:-208px}.md\:sl--my-56{margin-bottom:-224px;margin-top:-224px}.md\:sl--mx-56{margin-left:-224px;margin-right:-224px}.md\:sl--my-60{margin-bottom:-240px;margin-top:-240px}.md\:sl--mx-60{margin-left:-240px;margin-right:-240px}.md\:sl--my-64{margin-bottom:-256px;margin-top:-256px}.md\:sl--mx-64{margin-left:-256px;margin-right:-256px}.md\:sl--my-72{margin-bottom:-288px;margin-top:-288px}.md\:sl--mx-72{margin-left:-288px;margin-right:-288px}.md\:sl--my-80{margin-bottom:-320px;margin-top:-320px}.md\:sl--mx-80{margin-left:-320px;margin-right:-320px}.md\:sl--my-96{margin-bottom:-384px;margin-top:-384px}.md\:sl--mx-96{margin-left:-384px;margin-right:-384px}.md\:sl--my-px{margin-bottom:-1px;margin-top:-1px}.md\:sl--mx-px{margin-left:-1px;margin-right:-1px}.md\:sl--my-0\.5{margin-bottom:-2px;margin-top:-2px}.md\:sl--mx-0\.5{margin-left:-2px;margin-right:-2px}.md\:sl--my-1\.5{margin-bottom:-6px;margin-top:-6px}.md\:sl--mx-1\.5{margin-left:-6px;margin-right:-6px}.md\:sl--my-2\.5{margin-bottom:-10px;margin-top:-10px}.md\:sl--mx-2\.5{margin-left:-10px;margin-right:-10px}.md\:sl--my-3\.5{margin-bottom:-14px;margin-top:-14px}.md\:sl--mx-3\.5{margin-left:-14px;margin-right:-14px}.md\:sl--my-4\.5{margin-bottom:-18px;margin-top:-18px}.md\:sl--mx-4\.5{margin-left:-18px;margin-right:-18px}.md\:sl-mt-0{margin-top:0}.md\:sl-mr-0{margin-right:0}.md\:sl-mb-0{margin-bottom:0}.md\:sl-ml-0{margin-left:0}.md\:sl-mt-1{margin-top:4px}.md\:sl-mr-1{margin-right:4px}.md\:sl-mb-1{margin-bottom:4px}.md\:sl-ml-1{margin-left:4px}.md\:sl-mt-2{margin-top:8px}.md\:sl-mr-2{margin-right:8px}.md\:sl-mb-2{margin-bottom:8px}.md\:sl-ml-2{margin-left:8px}.md\:sl-mt-3{margin-top:12px}.md\:sl-mr-3{margin-right:12px}.md\:sl-mb-3{margin-bottom:12px}.md\:sl-ml-3{margin-left:12px}.md\:sl-mt-4{margin-top:16px}.md\:sl-mr-4{margin-right:16px}.md\:sl-mb-4{margin-bottom:16px}.md\:sl-ml-4{margin-left:16px}.md\:sl-mt-5{margin-top:20px}.md\:sl-mr-5{margin-right:20px}.md\:sl-mb-5{margin-bottom:20px}.md\:sl-ml-5{margin-left:20px}.md\:sl-mt-6{margin-top:24px}.md\:sl-mr-6{margin-right:24px}.md\:sl-mb-6{margin-bottom:24px}.md\:sl-ml-6{margin-left:24px}.md\:sl-mt-7{margin-top:28px}.md\:sl-mr-7{margin-right:28px}.md\:sl-mb-7{margin-bottom:28px}.md\:sl-ml-7{margin-left:28px}.md\:sl-mt-8{margin-top:32px}.md\:sl-mr-8{margin-right:32px}.md\:sl-mb-8{margin-bottom:32px}.md\:sl-ml-8{margin-left:32px}.md\:sl-mt-9{margin-top:36px}.md\:sl-mr-9{margin-right:36px}.md\:sl-mb-9{margin-bottom:36px}.md\:sl-ml-9{margin-left:36px}.md\:sl-mt-10{margin-top:40px}.md\:sl-mr-10{margin-right:40px}.md\:sl-mb-10{margin-bottom:40px}.md\:sl-ml-10{margin-left:40px}.md\:sl-mt-11{margin-top:44px}.md\:sl-mr-11{margin-right:44px}.md\:sl-mb-11{margin-bottom:44px}.md\:sl-ml-11{margin-left:44px}.md\:sl-mt-12{margin-top:48px}.md\:sl-mr-12{margin-right:48px}.md\:sl-mb-12{margin-bottom:48px}.md\:sl-ml-12{margin-left:48px}.md\:sl-mt-14{margin-top:56px}.md\:sl-mr-14{margin-right:56px}.md\:sl-mb-14{margin-bottom:56px}.md\:sl-ml-14{margin-left:56px}.md\:sl-mt-16{margin-top:64px}.md\:sl-mr-16{margin-right:64px}.md\:sl-mb-16{margin-bottom:64px}.md\:sl-ml-16{margin-left:64px}.md\:sl-mt-20{margin-top:80px}.md\:sl-mr-20{margin-right:80px}.md\:sl-mb-20{margin-bottom:80px}.md\:sl-ml-20{margin-left:80px}.md\:sl-mt-24{margin-top:96px}.md\:sl-mr-24{margin-right:96px}.md\:sl-mb-24{margin-bottom:96px}.md\:sl-ml-24{margin-left:96px}.md\:sl-mt-28{margin-top:112px}.md\:sl-mr-28{margin-right:112px}.md\:sl-mb-28{margin-bottom:112px}.md\:sl-ml-28{margin-left:112px}.md\:sl-mt-32{margin-top:128px}.md\:sl-mr-32{margin-right:128px}.md\:sl-mb-32{margin-bottom:128px}.md\:sl-ml-32{margin-left:128px}.md\:sl-mt-36{margin-top:144px}.md\:sl-mr-36{margin-right:144px}.md\:sl-mb-36{margin-bottom:144px}.md\:sl-ml-36{margin-left:144px}.md\:sl-mt-40{margin-top:160px}.md\:sl-mr-40{margin-right:160px}.md\:sl-mb-40{margin-bottom:160px}.md\:sl-ml-40{margin-left:160px}.md\:sl-mt-44{margin-top:176px}.md\:sl-mr-44{margin-right:176px}.md\:sl-mb-44{margin-bottom:176px}.md\:sl-ml-44{margin-left:176px}.md\:sl-mt-48{margin-top:192px}.md\:sl-mr-48{margin-right:192px}.md\:sl-mb-48{margin-bottom:192px}.md\:sl-ml-48{margin-left:192px}.md\:sl-mt-52{margin-top:208px}.md\:sl-mr-52{margin-right:208px}.md\:sl-mb-52{margin-bottom:208px}.md\:sl-ml-52{margin-left:208px}.md\:sl-mt-56{margin-top:224px}.md\:sl-mr-56{margin-right:224px}.md\:sl-mb-56{margin-bottom:224px}.md\:sl-ml-56{margin-left:224px}.md\:sl-mt-60{margin-top:240px}.md\:sl-mr-60{margin-right:240px}.md\:sl-mb-60{margin-bottom:240px}.md\:sl-ml-60{margin-left:240px}.md\:sl-mt-64{margin-top:256px}.md\:sl-mr-64{margin-right:256px}.md\:sl-mb-64{margin-bottom:256px}.md\:sl-ml-64{margin-left:256px}.md\:sl-mt-72{margin-top:288px}.md\:sl-mr-72{margin-right:288px}.md\:sl-mb-72{margin-bottom:288px}.md\:sl-ml-72{margin-left:288px}.md\:sl-mt-80{margin-top:320px}.md\:sl-mr-80{margin-right:320px}.md\:sl-mb-80{margin-bottom:320px}.md\:sl-ml-80{margin-left:320px}.md\:sl-mt-96{margin-top:384px}.md\:sl-mr-96{margin-right:384px}.md\:sl-mb-96{margin-bottom:384px}.md\:sl-ml-96{margin-left:384px}.md\:sl-mt-auto{margin-top:auto}.md\:sl-mr-auto{margin-right:auto}.md\:sl-mb-auto{margin-bottom:auto}.md\:sl-ml-auto{margin-left:auto}.md\:sl-mt-px{margin-top:1px}.md\:sl-mr-px{margin-right:1px}.md\:sl-mb-px{margin-bottom:1px}.md\:sl-ml-px{margin-left:1px}.md\:sl-mt-0\.5{margin-top:2px}.md\:sl-mr-0\.5{margin-right:2px}.md\:sl-mb-0\.5{margin-bottom:2px}.md\:sl-ml-0\.5{margin-left:2px}.md\:sl-mt-1\.5{margin-top:6px}.md\:sl-mr-1\.5{margin-right:6px}.md\:sl-mb-1\.5{margin-bottom:6px}.md\:sl-ml-1\.5{margin-left:6px}.md\:sl-mt-2\.5{margin-top:10px}.md\:sl-mr-2\.5{margin-right:10px}.md\:sl-mb-2\.5{margin-bottom:10px}.md\:sl-ml-2\.5{margin-left:10px}.md\:sl-mt-3\.5{margin-top:14px}.md\:sl-mr-3\.5{margin-right:14px}.md\:sl-mb-3\.5{margin-bottom:14px}.md\:sl-ml-3\.5{margin-left:14px}.md\:sl-mt-4\.5{margin-top:18px}.md\:sl-mr-4\.5{margin-right:18px}.md\:sl-mb-4\.5{margin-bottom:18px}.md\:sl-ml-4\.5{margin-left:18px}.md\:sl--mt-0{margin-top:0}.md\:sl--mr-0{margin-right:0}.md\:sl--mb-0{margin-bottom:0}.md\:sl--ml-0{margin-left:0}.md\:sl--mt-1{margin-top:-4px}.md\:sl--mr-1{margin-right:-4px}.md\:sl--mb-1{margin-bottom:-4px}.md\:sl--ml-1{margin-left:-4px}.md\:sl--mt-2{margin-top:-8px}.md\:sl--mr-2{margin-right:-8px}.md\:sl--mb-2{margin-bottom:-8px}.md\:sl--ml-2{margin-left:-8px}.md\:sl--mt-3{margin-top:-12px}.md\:sl--mr-3{margin-right:-12px}.md\:sl--mb-3{margin-bottom:-12px}.md\:sl--ml-3{margin-left:-12px}.md\:sl--mt-4{margin-top:-16px}.md\:sl--mr-4{margin-right:-16px}.md\:sl--mb-4{margin-bottom:-16px}.md\:sl--ml-4{margin-left:-16px}.md\:sl--mt-5{margin-top:-20px}.md\:sl--mr-5{margin-right:-20px}.md\:sl--mb-5{margin-bottom:-20px}.md\:sl--ml-5{margin-left:-20px}.md\:sl--mt-6{margin-top:-24px}.md\:sl--mr-6{margin-right:-24px}.md\:sl--mb-6{margin-bottom:-24px}.md\:sl--ml-6{margin-left:-24px}.md\:sl--mt-7{margin-top:-28px}.md\:sl--mr-7{margin-right:-28px}.md\:sl--mb-7{margin-bottom:-28px}.md\:sl--ml-7{margin-left:-28px}.md\:sl--mt-8{margin-top:-32px}.md\:sl--mr-8{margin-right:-32px}.md\:sl--mb-8{margin-bottom:-32px}.md\:sl--ml-8{margin-left:-32px}.md\:sl--mt-9{margin-top:-36px}.md\:sl--mr-9{margin-right:-36px}.md\:sl--mb-9{margin-bottom:-36px}.md\:sl--ml-9{margin-left:-36px}.md\:sl--mt-10{margin-top:-40px}.md\:sl--mr-10{margin-right:-40px}.md\:sl--mb-10{margin-bottom:-40px}.md\:sl--ml-10{margin-left:-40px}.md\:sl--mt-11{margin-top:-44px}.md\:sl--mr-11{margin-right:-44px}.md\:sl--mb-11{margin-bottom:-44px}.md\:sl--ml-11{margin-left:-44px}.md\:sl--mt-12{margin-top:-48px}.md\:sl--mr-12{margin-right:-48px}.md\:sl--mb-12{margin-bottom:-48px}.md\:sl--ml-12{margin-left:-48px}.md\:sl--mt-14{margin-top:-56px}.md\:sl--mr-14{margin-right:-56px}.md\:sl--mb-14{margin-bottom:-56px}.md\:sl--ml-14{margin-left:-56px}.md\:sl--mt-16{margin-top:-64px}.md\:sl--mr-16{margin-right:-64px}.md\:sl--mb-16{margin-bottom:-64px}.md\:sl--ml-16{margin-left:-64px}.md\:sl--mt-20{margin-top:-80px}.md\:sl--mr-20{margin-right:-80px}.md\:sl--mb-20{margin-bottom:-80px}.md\:sl--ml-20{margin-left:-80px}.md\:sl--mt-24{margin-top:-96px}.md\:sl--mr-24{margin-right:-96px}.md\:sl--mb-24{margin-bottom:-96px}.md\:sl--ml-24{margin-left:-96px}.md\:sl--mt-28{margin-top:-112px}.md\:sl--mr-28{margin-right:-112px}.md\:sl--mb-28{margin-bottom:-112px}.md\:sl--ml-28{margin-left:-112px}.md\:sl--mt-32{margin-top:-128px}.md\:sl--mr-32{margin-right:-128px}.md\:sl--mb-32{margin-bottom:-128px}.md\:sl--ml-32{margin-left:-128px}.md\:sl--mt-36{margin-top:-144px}.md\:sl--mr-36{margin-right:-144px}.md\:sl--mb-36{margin-bottom:-144px}.md\:sl--ml-36{margin-left:-144px}.md\:sl--mt-40{margin-top:-160px}.md\:sl--mr-40{margin-right:-160px}.md\:sl--mb-40{margin-bottom:-160px}.md\:sl--ml-40{margin-left:-160px}.md\:sl--mt-44{margin-top:-176px}.md\:sl--mr-44{margin-right:-176px}.md\:sl--mb-44{margin-bottom:-176px}.md\:sl--ml-44{margin-left:-176px}.md\:sl--mt-48{margin-top:-192px}.md\:sl--mr-48{margin-right:-192px}.md\:sl--mb-48{margin-bottom:-192px}.md\:sl--ml-48{margin-left:-192px}.md\:sl--mt-52{margin-top:-208px}.md\:sl--mr-52{margin-right:-208px}.md\:sl--mb-52{margin-bottom:-208px}.md\:sl--ml-52{margin-left:-208px}.md\:sl--mt-56{margin-top:-224px}.md\:sl--mr-56{margin-right:-224px}.md\:sl--mb-56{margin-bottom:-224px}.md\:sl--ml-56{margin-left:-224px}.md\:sl--mt-60{margin-top:-240px}.md\:sl--mr-60{margin-right:-240px}.md\:sl--mb-60{margin-bottom:-240px}.md\:sl--ml-60{margin-left:-240px}.md\:sl--mt-64{margin-top:-256px}.md\:sl--mr-64{margin-right:-256px}.md\:sl--mb-64{margin-bottom:-256px}.md\:sl--ml-64{margin-left:-256px}.md\:sl--mt-72{margin-top:-288px}.md\:sl--mr-72{margin-right:-288px}.md\:sl--mb-72{margin-bottom:-288px}.md\:sl--ml-72{margin-left:-288px}.md\:sl--mt-80{margin-top:-320px}.md\:sl--mr-80{margin-right:-320px}.md\:sl--mb-80{margin-bottom:-320px}.md\:sl--ml-80{margin-left:-320px}.md\:sl--mt-96{margin-top:-384px}.md\:sl--mr-96{margin-right:-384px}.md\:sl--mb-96{margin-bottom:-384px}.md\:sl--ml-96{margin-left:-384px}.md\:sl--mt-px{margin-top:-1px}.md\:sl--mr-px{margin-right:-1px}.md\:sl--mb-px{margin-bottom:-1px}.md\:sl--ml-px{margin-left:-1px}.md\:sl--mt-0\.5{margin-top:-2px}.md\:sl--mr-0\.5{margin-right:-2px}.md\:sl--mb-0\.5{margin-bottom:-2px}.md\:sl--ml-0\.5{margin-left:-2px}.md\:sl--mt-1\.5{margin-top:-6px}.md\:sl--mr-1\.5{margin-right:-6px}.md\:sl--mb-1\.5{margin-bottom:-6px}.md\:sl--ml-1\.5{margin-left:-6px}.md\:sl--mt-2\.5{margin-top:-10px}.md\:sl--mr-2\.5{margin-right:-10px}.md\:sl--mb-2\.5{margin-bottom:-10px}.md\:sl--ml-2\.5{margin-left:-10px}.md\:sl--mt-3\.5{margin-top:-14px}.md\:sl--mr-3\.5{margin-right:-14px}.md\:sl--mb-3\.5{margin-bottom:-14px}.md\:sl--ml-3\.5{margin-left:-14px}.md\:sl--mt-4\.5{margin-top:-18px}.md\:sl--mr-4\.5{margin-right:-18px}.md\:sl--mb-4\.5{margin-bottom:-18px}.md\:sl--ml-4\.5{margin-left:-18px}.md\:sl-max-h-full{max-height:100%}.md\:sl-max-h-screen{max-height:100vh}.md\:sl-max-w-none{max-width:none}.md\:sl-max-w-full{max-width:100%}.md\:sl-max-w-min{max-width:-moz-min-content;max-width:min-content}.md\:sl-max-w-max{max-width:-moz-max-content;max-width:max-content}.md\:sl-max-w-prose{max-width:65ch}.md\:sl-min-h-full{min-height:100%}.md\:sl-min-h-screen{min-height:100vh}.md\:sl-min-w-full{min-width:100%}.md\:sl-min-w-min{min-width:-moz-min-content;min-width:min-content}.md\:sl-min-w-max{min-width:-moz-max-content;min-width:max-content}.md\:sl-p-0{padding:0}.md\:sl-p-1{padding:4px}.md\:sl-p-2{padding:8px}.md\:sl-p-3{padding:12px}.md\:sl-p-4{padding:16px}.md\:sl-p-5{padding:20px}.md\:sl-p-6{padding:24px}.md\:sl-p-7{padding:28px}.md\:sl-p-8{padding:32px}.md\:sl-p-9{padding:36px}.md\:sl-p-10{padding:40px}.md\:sl-p-11{padding:44px}.md\:sl-p-12{padding:48px}.md\:sl-p-14{padding:56px}.md\:sl-p-16{padding:64px}.md\:sl-p-20{padding:80px}.md\:sl-p-24{padding:96px}.md\:sl-p-28{padding:112px}.md\:sl-p-32{padding:128px}.md\:sl-p-36{padding:144px}.md\:sl-p-40{padding:160px}.md\:sl-p-44{padding:176px}.md\:sl-p-48{padding:192px}.md\:sl-p-52{padding:208px}.md\:sl-p-56{padding:224px}.md\:sl-p-60{padding:240px}.md\:sl-p-64{padding:256px}.md\:sl-p-72{padding:288px}.md\:sl-p-80{padding:320px}.md\:sl-p-96{padding:384px}.md\:sl-p-px{padding:1px}.md\:sl-p-0\.5{padding:2px}.md\:sl-p-1\.5{padding:6px}.md\:sl-p-2\.5{padding:10px}.md\:sl-p-3\.5{padding:14px}.md\:sl-p-4\.5{padding:18px}.md\:sl-py-0{padding-bottom:0;padding-top:0}.md\:sl-px-0{padding-left:0;padding-right:0}.md\:sl-py-1{padding-bottom:4px;padding-top:4px}.md\:sl-px-1{padding-left:4px;padding-right:4px}.md\:sl-py-2{padding-bottom:8px;padding-top:8px}.md\:sl-px-2{padding-left:8px;padding-right:8px}.md\:sl-py-3{padding-bottom:12px;padding-top:12px}.md\:sl-px-3{padding-left:12px;padding-right:12px}.md\:sl-py-4{padding-bottom:16px;padding-top:16px}.md\:sl-px-4{padding-left:16px;padding-right:16px}.md\:sl-py-5{padding-bottom:20px;padding-top:20px}.md\:sl-px-5{padding-left:20px;padding-right:20px}.md\:sl-py-6{padding-bottom:24px;padding-top:24px}.md\:sl-px-6{padding-left:24px;padding-right:24px}.md\:sl-py-7{padding-bottom:28px;padding-top:28px}.md\:sl-px-7{padding-left:28px;padding-right:28px}.md\:sl-py-8{padding-bottom:32px;padding-top:32px}.md\:sl-px-8{padding-left:32px;padding-right:32px}.md\:sl-py-9{padding-bottom:36px;padding-top:36px}.md\:sl-px-9{padding-left:36px;padding-right:36px}.md\:sl-py-10{padding-bottom:40px;padding-top:40px}.md\:sl-px-10{padding-left:40px;padding-right:40px}.md\:sl-py-11{padding-bottom:44px;padding-top:44px}.md\:sl-px-11{padding-left:44px;padding-right:44px}.md\:sl-py-12{padding-bottom:48px;padding-top:48px}.md\:sl-px-12{padding-left:48px;padding-right:48px}.md\:sl-py-14{padding-bottom:56px;padding-top:56px}.md\:sl-px-14{padding-left:56px;padding-right:56px}.md\:sl-py-16{padding-bottom:64px;padding-top:64px}.md\:sl-px-16{padding-left:64px;padding-right:64px}.md\:sl-py-20{padding-bottom:80px;padding-top:80px}.md\:sl-px-20{padding-left:80px;padding-right:80px}.md\:sl-py-24{padding-bottom:96px;padding-top:96px}.md\:sl-px-24{padding-left:96px;padding-right:96px}.md\:sl-py-28{padding-bottom:112px;padding-top:112px}.md\:sl-px-28{padding-left:112px;padding-right:112px}.md\:sl-py-32{padding-bottom:128px;padding-top:128px}.md\:sl-px-32{padding-left:128px;padding-right:128px}.md\:sl-py-36{padding-bottom:144px;padding-top:144px}.md\:sl-px-36{padding-left:144px;padding-right:144px}.md\:sl-py-40{padding-bottom:160px;padding-top:160px}.md\:sl-px-40{padding-left:160px;padding-right:160px}.md\:sl-py-44{padding-bottom:176px;padding-top:176px}.md\:sl-px-44{padding-left:176px;padding-right:176px}.md\:sl-py-48{padding-bottom:192px;padding-top:192px}.md\:sl-px-48{padding-left:192px;padding-right:192px}.md\:sl-py-52{padding-bottom:208px;padding-top:208px}.md\:sl-px-52{padding-left:208px;padding-right:208px}.md\:sl-py-56{padding-bottom:224px;padding-top:224px}.md\:sl-px-56{padding-left:224px;padding-right:224px}.md\:sl-py-60{padding-bottom:240px;padding-top:240px}.md\:sl-px-60{padding-left:240px;padding-right:240px}.md\:sl-py-64{padding-bottom:256px;padding-top:256px}.md\:sl-px-64{padding-left:256px;padding-right:256px}.md\:sl-py-72{padding-bottom:288px;padding-top:288px}.md\:sl-px-72{padding-left:288px;padding-right:288px}.md\:sl-py-80{padding-bottom:320px;padding-top:320px}.md\:sl-px-80{padding-left:320px;padding-right:320px}.md\:sl-py-96{padding-bottom:384px;padding-top:384px}.md\:sl-px-96{padding-left:384px;padding-right:384px}.md\:sl-py-px{padding-bottom:1px;padding-top:1px}.md\:sl-px-px{padding-left:1px;padding-right:1px}.md\:sl-py-0\.5{padding-bottom:2px;padding-top:2px}.md\:sl-px-0\.5{padding-left:2px;padding-right:2px}.md\:sl-py-1\.5{padding-bottom:6px;padding-top:6px}.md\:sl-px-1\.5{padding-left:6px;padding-right:6px}.md\:sl-py-2\.5{padding-bottom:10px;padding-top:10px}.md\:sl-px-2\.5{padding-left:10px;padding-right:10px}.md\:sl-py-3\.5{padding-bottom:14px;padding-top:14px}.md\:sl-px-3\.5{padding-left:14px;padding-right:14px}.md\:sl-py-4\.5{padding-bottom:18px;padding-top:18px}.md\:sl-px-4\.5{padding-left:18px;padding-right:18px}.md\:sl-pt-0{padding-top:0}.md\:sl-pr-0{padding-right:0}.md\:sl-pb-0{padding-bottom:0}.md\:sl-pl-0{padding-left:0}.md\:sl-pt-1{padding-top:4px}.md\:sl-pr-1{padding-right:4px}.md\:sl-pb-1{padding-bottom:4px}.md\:sl-pl-1{padding-left:4px}.md\:sl-pt-2{padding-top:8px}.md\:sl-pr-2{padding-right:8px}.md\:sl-pb-2{padding-bottom:8px}.md\:sl-pl-2{padding-left:8px}.md\:sl-pt-3{padding-top:12px}.md\:sl-pr-3{padding-right:12px}.md\:sl-pb-3{padding-bottom:12px}.md\:sl-pl-3{padding-left:12px}.md\:sl-pt-4{padding-top:16px}.md\:sl-pr-4{padding-right:16px}.md\:sl-pb-4{padding-bottom:16px}.md\:sl-pl-4{padding-left:16px}.md\:sl-pt-5{padding-top:20px}.md\:sl-pr-5{padding-right:20px}.md\:sl-pb-5{padding-bottom:20px}.md\:sl-pl-5{padding-left:20px}.md\:sl-pt-6{padding-top:24px}.md\:sl-pr-6{padding-right:24px}.md\:sl-pb-6{padding-bottom:24px}.md\:sl-pl-6{padding-left:24px}.md\:sl-pt-7{padding-top:28px}.md\:sl-pr-7{padding-right:28px}.md\:sl-pb-7{padding-bottom:28px}.md\:sl-pl-7{padding-left:28px}.md\:sl-pt-8{padding-top:32px}.md\:sl-pr-8{padding-right:32px}.md\:sl-pb-8{padding-bottom:32px}.md\:sl-pl-8{padding-left:32px}.md\:sl-pt-9{padding-top:36px}.md\:sl-pr-9{padding-right:36px}.md\:sl-pb-9{padding-bottom:36px}.md\:sl-pl-9{padding-left:36px}.md\:sl-pt-10{padding-top:40px}.md\:sl-pr-10{padding-right:40px}.md\:sl-pb-10{padding-bottom:40px}.md\:sl-pl-10{padding-left:40px}.md\:sl-pt-11{padding-top:44px}.md\:sl-pr-11{padding-right:44px}.md\:sl-pb-11{padding-bottom:44px}.md\:sl-pl-11{padding-left:44px}.md\:sl-pt-12{padding-top:48px}.md\:sl-pr-12{padding-right:48px}.md\:sl-pb-12{padding-bottom:48px}.md\:sl-pl-12{padding-left:48px}.md\:sl-pt-14{padding-top:56px}.md\:sl-pr-14{padding-right:56px}.md\:sl-pb-14{padding-bottom:56px}.md\:sl-pl-14{padding-left:56px}.md\:sl-pt-16{padding-top:64px}.md\:sl-pr-16{padding-right:64px}.md\:sl-pb-16{padding-bottom:64px}.md\:sl-pl-16{padding-left:64px}.md\:sl-pt-20{padding-top:80px}.md\:sl-pr-20{padding-right:80px}.md\:sl-pb-20{padding-bottom:80px}.md\:sl-pl-20{padding-left:80px}.md\:sl-pt-24{padding-top:96px}.md\:sl-pr-24{padding-right:96px}.md\:sl-pb-24{padding-bottom:96px}.md\:sl-pl-24{padding-left:96px}.md\:sl-pt-28{padding-top:112px}.md\:sl-pr-28{padding-right:112px}.md\:sl-pb-28{padding-bottom:112px}.md\:sl-pl-28{padding-left:112px}.md\:sl-pt-32{padding-top:128px}.md\:sl-pr-32{padding-right:128px}.md\:sl-pb-32{padding-bottom:128px}.md\:sl-pl-32{padding-left:128px}.md\:sl-pt-36{padding-top:144px}.md\:sl-pr-36{padding-right:144px}.md\:sl-pb-36{padding-bottom:144px}.md\:sl-pl-36{padding-left:144px}.md\:sl-pt-40{padding-top:160px}.md\:sl-pr-40{padding-right:160px}.md\:sl-pb-40{padding-bottom:160px}.md\:sl-pl-40{padding-left:160px}.md\:sl-pt-44{padding-top:176px}.md\:sl-pr-44{padding-right:176px}.md\:sl-pb-44{padding-bottom:176px}.md\:sl-pl-44{padding-left:176px}.md\:sl-pt-48{padding-top:192px}.md\:sl-pr-48{padding-right:192px}.md\:sl-pb-48{padding-bottom:192px}.md\:sl-pl-48{padding-left:192px}.md\:sl-pt-52{padding-top:208px}.md\:sl-pr-52{padding-right:208px}.md\:sl-pb-52{padding-bottom:208px}.md\:sl-pl-52{padding-left:208px}.md\:sl-pt-56{padding-top:224px}.md\:sl-pr-56{padding-right:224px}.md\:sl-pb-56{padding-bottom:224px}.md\:sl-pl-56{padding-left:224px}.md\:sl-pt-60{padding-top:240px}.md\:sl-pr-60{padding-right:240px}.md\:sl-pb-60{padding-bottom:240px}.md\:sl-pl-60{padding-left:240px}.md\:sl-pt-64{padding-top:256px}.md\:sl-pr-64{padding-right:256px}.md\:sl-pb-64{padding-bottom:256px}.md\:sl-pl-64{padding-left:256px}.md\:sl-pt-72{padding-top:288px}.md\:sl-pr-72{padding-right:288px}.md\:sl-pb-72{padding-bottom:288px}.md\:sl-pl-72{padding-left:288px}.md\:sl-pt-80{padding-top:320px}.md\:sl-pr-80{padding-right:320px}.md\:sl-pb-80{padding-bottom:320px}.md\:sl-pl-80{padding-left:320px}.md\:sl-pt-96{padding-top:384px}.md\:sl-pr-96{padding-right:384px}.md\:sl-pb-96{padding-bottom:384px}.md\:sl-pl-96{padding-left:384px}.md\:sl-pt-px{padding-top:1px}.md\:sl-pr-px{padding-right:1px}.md\:sl-pb-px{padding-bottom:1px}.md\:sl-pl-px{padding-left:1px}.md\:sl-pt-0\.5{padding-top:2px}.md\:sl-pr-0\.5{padding-right:2px}.md\:sl-pb-0\.5{padding-bottom:2px}.md\:sl-pl-0\.5{padding-left:2px}.md\:sl-pt-1\.5{padding-top:6px}.md\:sl-pr-1\.5{padding-right:6px}.md\:sl-pb-1\.5{padding-bottom:6px}.md\:sl-pl-1\.5{padding-left:6px}.md\:sl-pt-2\.5{padding-top:10px}.md\:sl-pr-2\.5{padding-right:10px}.md\:sl-pb-2\.5{padding-bottom:10px}.md\:sl-pl-2\.5{padding-left:10px}.md\:sl-pt-3\.5{padding-top:14px}.md\:sl-pr-3\.5{padding-right:14px}.md\:sl-pb-3\.5{padding-bottom:14px}.md\:sl-pl-3\.5{padding-left:14px}.md\:sl-pt-4\.5{padding-top:18px}.md\:sl-pr-4\.5{padding-right:18px}.md\:sl-pb-4\.5{padding-bottom:18px}.md\:sl-pl-4\.5{padding-left:18px}.md\:sl-static{position:static}.md\:sl-fixed{position:fixed}.md\:sl-absolute{position:absolute}.md\:sl-relative{position:relative}.md\:sl-sticky{position:-webkit-sticky;position:sticky}.md\:sl-visible{visibility:visible}.md\:sl-invisible{visibility:hidden}.sl-group:hover .md\:group-hover\:sl-visible{visibility:visible}.sl-group:hover .md\:group-hover\:sl-invisible{visibility:hidden}.sl-group:focus .md\:group-focus\:sl-visible{visibility:visible}.sl-group:focus .md\:group-focus\:sl-invisible{visibility:hidden}.md\:sl-w-0{width:0}.md\:sl-w-1{width:4px}.md\:sl-w-2{width:8px}.md\:sl-w-3{width:12px}.md\:sl-w-4{width:16px}.md\:sl-w-5{width:20px}.md\:sl-w-6{width:24px}.md\:sl-w-7{width:28px}.md\:sl-w-8{width:32px}.md\:sl-w-9{width:36px}.md\:sl-w-10{width:40px}.md\:sl-w-11{width:44px}.md\:sl-w-12{width:48px}.md\:sl-w-14{width:56px}.md\:sl-w-16{width:64px}.md\:sl-w-20{width:80px}.md\:sl-w-24{width:96px}.md\:sl-w-28{width:112px}.md\:sl-w-32{width:128px}.md\:sl-w-36{width:144px}.md\:sl-w-40{width:160px}.md\:sl-w-44{width:176px}.md\:sl-w-48{width:192px}.md\:sl-w-52{width:208px}.md\:sl-w-56{width:224px}.md\:sl-w-60{width:240px}.md\:sl-w-64{width:256px}.md\:sl-w-72{width:288px}.md\:sl-w-80{width:320px}.md\:sl-w-96{width:384px}.md\:sl-w-auto{width:auto}.md\:sl-w-px{width:1px}.md\:sl-w-0\.5{width:2px}.md\:sl-w-1\.5{width:6px}.md\:sl-w-2\.5{width:10px}.md\:sl-w-3\.5{width:14px}.md\:sl-w-4\.5{width:18px}.md\:sl-w-xs{width:20px}.md\:sl-w-sm{width:24px}.md\:sl-w-md{width:32px}.md\:sl-w-lg{width:36px}.md\:sl-w-xl{width:44px}.md\:sl-w-2xl{width:52px}.md\:sl-w-3xl{width:60px}.md\:sl-w-1\/2{width:50%}.md\:sl-w-1\/3{width:33.333333%}.md\:sl-w-2\/3{width:66.666667%}.md\:sl-w-1\/4{width:25%}.md\:sl-w-2\/4{width:50%}.md\:sl-w-3\/4{width:75%}.md\:sl-w-1\/5{width:20%}.md\:sl-w-2\/5{width:40%}.md\:sl-w-3\/5{width:60%}.md\:sl-w-4\/5{width:80%}.md\:sl-w-1\/6{width:16.666667%}.md\:sl-w-2\/6{width:33.333333%}.md\:sl-w-3\/6{width:50%}.md\:sl-w-4\/6{width:66.666667%}.md\:sl-w-5\/6{width:83.333333%}.md\:sl-w-full{width:100%}.md\:sl-w-screen{width:100vw}.md\:sl-w-min{width:-moz-min-content;width:min-content}.md\:sl-w-max{width:-moz-max-content;width:max-content}}@media (max-width:975px){.lg\:sl-stack--1{gap:4px}.lg\:sl-stack--2{gap:8px}.lg\:sl-stack--3{gap:12px}.lg\:sl-stack--4{gap:16px}.lg\:sl-stack--5{gap:20px}.lg\:sl-stack--6{gap:24px}.lg\:sl-stack--7{gap:28px}.lg\:sl-stack--8{gap:32px}.lg\:sl-stack--9{gap:36px}.lg\:sl-stack--10{gap:40px}.lg\:sl-stack--12{gap:48px}.lg\:sl-stack--14{gap:56px}.lg\:sl-stack--16{gap:64px}.lg\:sl-stack--20{gap:80px}.lg\:sl-stack--24{gap:96px}.lg\:sl-stack--32{gap:128px}.lg\:sl-content-center{align-content:center}.lg\:sl-content-start{align-content:flex-start}.lg\:sl-content-end{align-content:flex-end}.lg\:sl-content-between{align-content:space-between}.lg\:sl-content-around{align-content:space-around}.lg\:sl-content-evenly{align-content:space-evenly}.lg\:sl-items-start{align-items:flex-start}.lg\:sl-items-end{align-items:flex-end}.lg\:sl-items-center{align-items:center}.lg\:sl-items-baseline{align-items:baseline}.lg\:sl-items-stretch{align-items:stretch}.lg\:sl-self-auto{align-self:auto}.lg\:sl-self-start{align-self:flex-start}.lg\:sl-self-end{align-self:flex-end}.lg\:sl-self-center{align-self:center}.lg\:sl-self-stretch{align-self:stretch}.lg\:sl-blur-0,.lg\:sl-blur-none{--tw-blur:blur(0)}.lg\:sl-blur-sm{--tw-blur:blur(4px)}.lg\:sl-blur{--tw-blur:blur(8px)}.lg\:sl-blur-md{--tw-blur:blur(12px)}.lg\:sl-blur-lg{--tw-blur:blur(16px)}.lg\:sl-blur-xl{--tw-blur:blur(24px)}.lg\:sl-blur-2xl{--tw-blur:blur(40px)}.lg\:sl-blur-3xl{--tw-blur:blur(64px)}.lg\:sl-block{display:block}.lg\:sl-inline-block{display:inline-block}.lg\:sl-inline{display:inline}.lg\:sl-flex{display:flex}.lg\:sl-inline-flex{display:inline-flex}.lg\:sl-table{display:table}.lg\:sl-inline-table{display:inline-table}.lg\:sl-table-caption{display:table-caption}.lg\:sl-table-cell{display:table-cell}.lg\:sl-table-column{display:table-column}.lg\:sl-table-column-group{display:table-column-group}.lg\:sl-table-footer-group{display:table-footer-group}.lg\:sl-table-header-group{display:table-header-group}.lg\:sl-table-row-group{display:table-row-group}.lg\:sl-table-row{display:table-row}.lg\:sl-flow-root{display:flow-root}.lg\:sl-grid{display:grid}.lg\:sl-inline-grid{display:inline-grid}.lg\:sl-contents{display:contents}.lg\:sl-list-item{display:list-item}.lg\:sl-hidden{display:none}.lg\:sl-drop-shadow{--tw-drop-shadow:drop-shadow(var(--drop-shadow-default1)) drop-shadow(var(--drop-shadow-default2))}.lg\:sl-flex-1{flex:1 1}.lg\:sl-flex-auto{flex:1 1 auto}.lg\:sl-flex-initial{flex:0 1 auto}.lg\:sl-flex-none{flex:none}.lg\:sl-flex-row{flex-direction:row}.lg\:sl-flex-row-reverse{flex-direction:row-reverse}.lg\:sl-flex-col{flex-direction:column}.lg\:sl-flex-col-reverse{flex-direction:column-reverse}.lg\:sl-flex-grow-0{flex-grow:0}.lg\:sl-flex-grow{flex-grow:1}.lg\:sl-flex-shrink-0{flex-shrink:0}.lg\:sl-flex-shrink{flex-shrink:1}.lg\:sl-flex-wrap{flex-wrap:wrap}.lg\:sl-flex-wrap-reverse{flex-wrap:wrap-reverse}.lg\:sl-flex-nowrap{flex-wrap:nowrap}.lg\:sl-h-0{height:0}.lg\:sl-h-1{height:4px}.lg\:sl-h-2{height:8px}.lg\:sl-h-3{height:12px}.lg\:sl-h-4{height:16px}.lg\:sl-h-5{height:20px}.lg\:sl-h-6{height:24px}.lg\:sl-h-7{height:28px}.lg\:sl-h-8{height:32px}.lg\:sl-h-9{height:36px}.lg\:sl-h-10{height:40px}.lg\:sl-h-11{height:44px}.lg\:sl-h-12{height:48px}.lg\:sl-h-14{height:56px}.lg\:sl-h-16{height:64px}.lg\:sl-h-20{height:80px}.lg\:sl-h-24{height:96px}.lg\:sl-h-28{height:112px}.lg\:sl-h-32{height:128px}.lg\:sl-h-36{height:144px}.lg\:sl-h-40{height:160px}.lg\:sl-h-44{height:176px}.lg\:sl-h-48{height:192px}.lg\:sl-h-52{height:208px}.lg\:sl-h-56{height:224px}.lg\:sl-h-60{height:240px}.lg\:sl-h-64{height:256px}.lg\:sl-h-72{height:288px}.lg\:sl-h-80{height:320px}.lg\:sl-h-96{height:384px}.lg\:sl-h-auto{height:auto}.lg\:sl-h-px{height:1px}.lg\:sl-h-0\.5{height:2px}.lg\:sl-h-1\.5{height:6px}.lg\:sl-h-2\.5{height:10px}.lg\:sl-h-3\.5{height:14px}.lg\:sl-h-4\.5{height:18px}.lg\:sl-h-xs{height:20px}.lg\:sl-h-sm{height:24px}.lg\:sl-h-md{height:32px}.lg\:sl-h-lg{height:36px}.lg\:sl-h-xl{height:44px}.lg\:sl-h-2xl{height:52px}.lg\:sl-h-3xl{height:60px}.lg\:sl-h-full{height:100%}.lg\:sl-h-screen{height:100vh}.lg\:sl-justify-start{justify-content:flex-start}.lg\:sl-justify-end{justify-content:flex-end}.lg\:sl-justify-center{justify-content:center}.lg\:sl-justify-between{justify-content:space-between}.lg\:sl-justify-around{justify-content:space-around}.lg\:sl-justify-evenly{justify-content:space-evenly}.lg\:sl-justify-items-start{justify-items:start}.lg\:sl-justify-items-end{justify-items:end}.lg\:sl-justify-items-center{justify-items:center}.lg\:sl-justify-items-stretch{justify-items:stretch}.lg\:sl-justify-self-auto{justify-self:auto}.lg\:sl-justify-self-start{justify-self:start}.lg\:sl-justify-self-end{justify-self:end}.lg\:sl-justify-self-center{justify-self:center}.lg\:sl-justify-self-stretch{justify-self:stretch}.lg\:sl-m-0{margin:0}.lg\:sl-m-1{margin:4px}.lg\:sl-m-2{margin:8px}.lg\:sl-m-3{margin:12px}.lg\:sl-m-4{margin:16px}.lg\:sl-m-5{margin:20px}.lg\:sl-m-6{margin:24px}.lg\:sl-m-7{margin:28px}.lg\:sl-m-8{margin:32px}.lg\:sl-m-9{margin:36px}.lg\:sl-m-10{margin:40px}.lg\:sl-m-11{margin:44px}.lg\:sl-m-12{margin:48px}.lg\:sl-m-14{margin:56px}.lg\:sl-m-16{margin:64px}.lg\:sl-m-20{margin:80px}.lg\:sl-m-24{margin:96px}.lg\:sl-m-28{margin:112px}.lg\:sl-m-32{margin:128px}.lg\:sl-m-36{margin:144px}.lg\:sl-m-40{margin:160px}.lg\:sl-m-44{margin:176px}.lg\:sl-m-48{margin:192px}.lg\:sl-m-52{margin:208px}.lg\:sl-m-56{margin:224px}.lg\:sl-m-60{margin:240px}.lg\:sl-m-64{margin:256px}.lg\:sl-m-72{margin:288px}.lg\:sl-m-80{margin:320px}.lg\:sl-m-96{margin:384px}.lg\:sl-m-auto{margin:auto}.lg\:sl-m-px{margin:1px}.lg\:sl-m-0\.5{margin:2px}.lg\:sl-m-1\.5{margin:6px}.lg\:sl-m-2\.5{margin:10px}.lg\:sl-m-3\.5{margin:14px}.lg\:sl-m-4\.5{margin:18px}.lg\:sl--m-0{margin:0}.lg\:sl--m-1{margin:-4px}.lg\:sl--m-2{margin:-8px}.lg\:sl--m-3{margin:-12px}.lg\:sl--m-4{margin:-16px}.lg\:sl--m-5{margin:-20px}.lg\:sl--m-6{margin:-24px}.lg\:sl--m-7{margin:-28px}.lg\:sl--m-8{margin:-32px}.lg\:sl--m-9{margin:-36px}.lg\:sl--m-10{margin:-40px}.lg\:sl--m-11{margin:-44px}.lg\:sl--m-12{margin:-48px}.lg\:sl--m-14{margin:-56px}.lg\:sl--m-16{margin:-64px}.lg\:sl--m-20{margin:-80px}.lg\:sl--m-24{margin:-96px}.lg\:sl--m-28{margin:-112px}.lg\:sl--m-32{margin:-128px}.lg\:sl--m-36{margin:-144px}.lg\:sl--m-40{margin:-160px}.lg\:sl--m-44{margin:-176px}.lg\:sl--m-48{margin:-192px}.lg\:sl--m-52{margin:-208px}.lg\:sl--m-56{margin:-224px}.lg\:sl--m-60{margin:-240px}.lg\:sl--m-64{margin:-256px}.lg\:sl--m-72{margin:-288px}.lg\:sl--m-80{margin:-320px}.lg\:sl--m-96{margin:-384px}.lg\:sl--m-px{margin:-1px}.lg\:sl--m-0\.5{margin:-2px}.lg\:sl--m-1\.5{margin:-6px}.lg\:sl--m-2\.5{margin:-10px}.lg\:sl--m-3\.5{margin:-14px}.lg\:sl--m-4\.5{margin:-18px}.lg\:sl-my-0{margin-bottom:0;margin-top:0}.lg\:sl-mx-0{margin-left:0;margin-right:0}.lg\:sl-my-1{margin-bottom:4px;margin-top:4px}.lg\:sl-mx-1{margin-left:4px;margin-right:4px}.lg\:sl-my-2{margin-bottom:8px;margin-top:8px}.lg\:sl-mx-2{margin-left:8px;margin-right:8px}.lg\:sl-my-3{margin-bottom:12px;margin-top:12px}.lg\:sl-mx-3{margin-left:12px;margin-right:12px}.lg\:sl-my-4{margin-bottom:16px;margin-top:16px}.lg\:sl-mx-4{margin-left:16px;margin-right:16px}.lg\:sl-my-5{margin-bottom:20px;margin-top:20px}.lg\:sl-mx-5{margin-left:20px;margin-right:20px}.lg\:sl-my-6{margin-bottom:24px;margin-top:24px}.lg\:sl-mx-6{margin-left:24px;margin-right:24px}.lg\:sl-my-7{margin-bottom:28px;margin-top:28px}.lg\:sl-mx-7{margin-left:28px;margin-right:28px}.lg\:sl-my-8{margin-bottom:32px;margin-top:32px}.lg\:sl-mx-8{margin-left:32px;margin-right:32px}.lg\:sl-my-9{margin-bottom:36px;margin-top:36px}.lg\:sl-mx-9{margin-left:36px;margin-right:36px}.lg\:sl-my-10{margin-bottom:40px;margin-top:40px}.lg\:sl-mx-10{margin-left:40px;margin-right:40px}.lg\:sl-my-11{margin-bottom:44px;margin-top:44px}.lg\:sl-mx-11{margin-left:44px;margin-right:44px}.lg\:sl-my-12{margin-bottom:48px;margin-top:48px}.lg\:sl-mx-12{margin-left:48px;margin-right:48px}.lg\:sl-my-14{margin-bottom:56px;margin-top:56px}.lg\:sl-mx-14{margin-left:56px;margin-right:56px}.lg\:sl-my-16{margin-bottom:64px;margin-top:64px}.lg\:sl-mx-16{margin-left:64px;margin-right:64px}.lg\:sl-my-20{margin-bottom:80px;margin-top:80px}.lg\:sl-mx-20{margin-left:80px;margin-right:80px}.lg\:sl-my-24{margin-bottom:96px;margin-top:96px}.lg\:sl-mx-24{margin-left:96px;margin-right:96px}.lg\:sl-my-28{margin-bottom:112px;margin-top:112px}.lg\:sl-mx-28{margin-left:112px;margin-right:112px}.lg\:sl-my-32{margin-bottom:128px;margin-top:128px}.lg\:sl-mx-32{margin-left:128px;margin-right:128px}.lg\:sl-my-36{margin-bottom:144px;margin-top:144px}.lg\:sl-mx-36{margin-left:144px;margin-right:144px}.lg\:sl-my-40{margin-bottom:160px;margin-top:160px}.lg\:sl-mx-40{margin-left:160px;margin-right:160px}.lg\:sl-my-44{margin-bottom:176px;margin-top:176px}.lg\:sl-mx-44{margin-left:176px;margin-right:176px}.lg\:sl-my-48{margin-bottom:192px;margin-top:192px}.lg\:sl-mx-48{margin-left:192px;margin-right:192px}.lg\:sl-my-52{margin-bottom:208px;margin-top:208px}.lg\:sl-mx-52{margin-left:208px;margin-right:208px}.lg\:sl-my-56{margin-bottom:224px;margin-top:224px}.lg\:sl-mx-56{margin-left:224px;margin-right:224px}.lg\:sl-my-60{margin-bottom:240px;margin-top:240px}.lg\:sl-mx-60{margin-left:240px;margin-right:240px}.lg\:sl-my-64{margin-bottom:256px;margin-top:256px}.lg\:sl-mx-64{margin-left:256px;margin-right:256px}.lg\:sl-my-72{margin-bottom:288px;margin-top:288px}.lg\:sl-mx-72{margin-left:288px;margin-right:288px}.lg\:sl-my-80{margin-bottom:320px;margin-top:320px}.lg\:sl-mx-80{margin-left:320px;margin-right:320px}.lg\:sl-my-96{margin-bottom:384px;margin-top:384px}.lg\:sl-mx-96{margin-left:384px;margin-right:384px}.lg\:sl-my-auto{margin-bottom:auto;margin-top:auto}.lg\:sl-mx-auto{margin-left:auto;margin-right:auto}.lg\:sl-my-px{margin-bottom:1px;margin-top:1px}.lg\:sl-mx-px{margin-left:1px;margin-right:1px}.lg\:sl-my-0\.5{margin-bottom:2px;margin-top:2px}.lg\:sl-mx-0\.5{margin-left:2px;margin-right:2px}.lg\:sl-my-1\.5{margin-bottom:6px;margin-top:6px}.lg\:sl-mx-1\.5{margin-left:6px;margin-right:6px}.lg\:sl-my-2\.5{margin-bottom:10px;margin-top:10px}.lg\:sl-mx-2\.5{margin-left:10px;margin-right:10px}.lg\:sl-my-3\.5{margin-bottom:14px;margin-top:14px}.lg\:sl-mx-3\.5{margin-left:14px;margin-right:14px}.lg\:sl-my-4\.5{margin-bottom:18px;margin-top:18px}.lg\:sl-mx-4\.5{margin-left:18px;margin-right:18px}.lg\:sl--my-0{margin-bottom:0;margin-top:0}.lg\:sl--mx-0{margin-left:0;margin-right:0}.lg\:sl--my-1{margin-bottom:-4px;margin-top:-4px}.lg\:sl--mx-1{margin-left:-4px;margin-right:-4px}.lg\:sl--my-2{margin-bottom:-8px;margin-top:-8px}.lg\:sl--mx-2{margin-left:-8px;margin-right:-8px}.lg\:sl--my-3{margin-bottom:-12px;margin-top:-12px}.lg\:sl--mx-3{margin-left:-12px;margin-right:-12px}.lg\:sl--my-4{margin-bottom:-16px;margin-top:-16px}.lg\:sl--mx-4{margin-left:-16px;margin-right:-16px}.lg\:sl--my-5{margin-bottom:-20px;margin-top:-20px}.lg\:sl--mx-5{margin-left:-20px;margin-right:-20px}.lg\:sl--my-6{margin-bottom:-24px;margin-top:-24px}.lg\:sl--mx-6{margin-left:-24px;margin-right:-24px}.lg\:sl--my-7{margin-bottom:-28px;margin-top:-28px}.lg\:sl--mx-7{margin-left:-28px;margin-right:-28px}.lg\:sl--my-8{margin-bottom:-32px;margin-top:-32px}.lg\:sl--mx-8{margin-left:-32px;margin-right:-32px}.lg\:sl--my-9{margin-bottom:-36px;margin-top:-36px}.lg\:sl--mx-9{margin-left:-36px;margin-right:-36px}.lg\:sl--my-10{margin-bottom:-40px;margin-top:-40px}.lg\:sl--mx-10{margin-left:-40px;margin-right:-40px}.lg\:sl--my-11{margin-bottom:-44px;margin-top:-44px}.lg\:sl--mx-11{margin-left:-44px;margin-right:-44px}.lg\:sl--my-12{margin-bottom:-48px;margin-top:-48px}.lg\:sl--mx-12{margin-left:-48px;margin-right:-48px}.lg\:sl--my-14{margin-bottom:-56px;margin-top:-56px}.lg\:sl--mx-14{margin-left:-56px;margin-right:-56px}.lg\:sl--my-16{margin-bottom:-64px;margin-top:-64px}.lg\:sl--mx-16{margin-left:-64px;margin-right:-64px}.lg\:sl--my-20{margin-bottom:-80px;margin-top:-80px}.lg\:sl--mx-20{margin-left:-80px;margin-right:-80px}.lg\:sl--my-24{margin-bottom:-96px;margin-top:-96px}.lg\:sl--mx-24{margin-left:-96px;margin-right:-96px}.lg\:sl--my-28{margin-bottom:-112px;margin-top:-112px}.lg\:sl--mx-28{margin-left:-112px;margin-right:-112px}.lg\:sl--my-32{margin-bottom:-128px;margin-top:-128px}.lg\:sl--mx-32{margin-left:-128px;margin-right:-128px}.lg\:sl--my-36{margin-bottom:-144px;margin-top:-144px}.lg\:sl--mx-36{margin-left:-144px;margin-right:-144px}.lg\:sl--my-40{margin-bottom:-160px;margin-top:-160px}.lg\:sl--mx-40{margin-left:-160px;margin-right:-160px}.lg\:sl--my-44{margin-bottom:-176px;margin-top:-176px}.lg\:sl--mx-44{margin-left:-176px;margin-right:-176px}.lg\:sl--my-48{margin-bottom:-192px;margin-top:-192px}.lg\:sl--mx-48{margin-left:-192px;margin-right:-192px}.lg\:sl--my-52{margin-bottom:-208px;margin-top:-208px}.lg\:sl--mx-52{margin-left:-208px;margin-right:-208px}.lg\:sl--my-56{margin-bottom:-224px;margin-top:-224px}.lg\:sl--mx-56{margin-left:-224px;margin-right:-224px}.lg\:sl--my-60{margin-bottom:-240px;margin-top:-240px}.lg\:sl--mx-60{margin-left:-240px;margin-right:-240px}.lg\:sl--my-64{margin-bottom:-256px;margin-top:-256px}.lg\:sl--mx-64{margin-left:-256px;margin-right:-256px}.lg\:sl--my-72{margin-bottom:-288px;margin-top:-288px}.lg\:sl--mx-72{margin-left:-288px;margin-right:-288px}.lg\:sl--my-80{margin-bottom:-320px;margin-top:-320px}.lg\:sl--mx-80{margin-left:-320px;margin-right:-320px}.lg\:sl--my-96{margin-bottom:-384px;margin-top:-384px}.lg\:sl--mx-96{margin-left:-384px;margin-right:-384px}.lg\:sl--my-px{margin-bottom:-1px;margin-top:-1px}.lg\:sl--mx-px{margin-left:-1px;margin-right:-1px}.lg\:sl--my-0\.5{margin-bottom:-2px;margin-top:-2px}.lg\:sl--mx-0\.5{margin-left:-2px;margin-right:-2px}.lg\:sl--my-1\.5{margin-bottom:-6px;margin-top:-6px}.lg\:sl--mx-1\.5{margin-left:-6px;margin-right:-6px}.lg\:sl--my-2\.5{margin-bottom:-10px;margin-top:-10px}.lg\:sl--mx-2\.5{margin-left:-10px;margin-right:-10px}.lg\:sl--my-3\.5{margin-bottom:-14px;margin-top:-14px}.lg\:sl--mx-3\.5{margin-left:-14px;margin-right:-14px}.lg\:sl--my-4\.5{margin-bottom:-18px;margin-top:-18px}.lg\:sl--mx-4\.5{margin-left:-18px;margin-right:-18px}.lg\:sl-mt-0{margin-top:0}.lg\:sl-mr-0{margin-right:0}.lg\:sl-mb-0{margin-bottom:0}.lg\:sl-ml-0{margin-left:0}.lg\:sl-mt-1{margin-top:4px}.lg\:sl-mr-1{margin-right:4px}.lg\:sl-mb-1{margin-bottom:4px}.lg\:sl-ml-1{margin-left:4px}.lg\:sl-mt-2{margin-top:8px}.lg\:sl-mr-2{margin-right:8px}.lg\:sl-mb-2{margin-bottom:8px}.lg\:sl-ml-2{margin-left:8px}.lg\:sl-mt-3{margin-top:12px}.lg\:sl-mr-3{margin-right:12px}.lg\:sl-mb-3{margin-bottom:12px}.lg\:sl-ml-3{margin-left:12px}.lg\:sl-mt-4{margin-top:16px}.lg\:sl-mr-4{margin-right:16px}.lg\:sl-mb-4{margin-bottom:16px}.lg\:sl-ml-4{margin-left:16px}.lg\:sl-mt-5{margin-top:20px}.lg\:sl-mr-5{margin-right:20px}.lg\:sl-mb-5{margin-bottom:20px}.lg\:sl-ml-5{margin-left:20px}.lg\:sl-mt-6{margin-top:24px}.lg\:sl-mr-6{margin-right:24px}.lg\:sl-mb-6{margin-bottom:24px}.lg\:sl-ml-6{margin-left:24px}.lg\:sl-mt-7{margin-top:28px}.lg\:sl-mr-7{margin-right:28px}.lg\:sl-mb-7{margin-bottom:28px}.lg\:sl-ml-7{margin-left:28px}.lg\:sl-mt-8{margin-top:32px}.lg\:sl-mr-8{margin-right:32px}.lg\:sl-mb-8{margin-bottom:32px}.lg\:sl-ml-8{margin-left:32px}.lg\:sl-mt-9{margin-top:36px}.lg\:sl-mr-9{margin-right:36px}.lg\:sl-mb-9{margin-bottom:36px}.lg\:sl-ml-9{margin-left:36px}.lg\:sl-mt-10{margin-top:40px}.lg\:sl-mr-10{margin-right:40px}.lg\:sl-mb-10{margin-bottom:40px}.lg\:sl-ml-10{margin-left:40px}.lg\:sl-mt-11{margin-top:44px}.lg\:sl-mr-11{margin-right:44px}.lg\:sl-mb-11{margin-bottom:44px}.lg\:sl-ml-11{margin-left:44px}.lg\:sl-mt-12{margin-top:48px}.lg\:sl-mr-12{margin-right:48px}.lg\:sl-mb-12{margin-bottom:48px}.lg\:sl-ml-12{margin-left:48px}.lg\:sl-mt-14{margin-top:56px}.lg\:sl-mr-14{margin-right:56px}.lg\:sl-mb-14{margin-bottom:56px}.lg\:sl-ml-14{margin-left:56px}.lg\:sl-mt-16{margin-top:64px}.lg\:sl-mr-16{margin-right:64px}.lg\:sl-mb-16{margin-bottom:64px}.lg\:sl-ml-16{margin-left:64px}.lg\:sl-mt-20{margin-top:80px}.lg\:sl-mr-20{margin-right:80px}.lg\:sl-mb-20{margin-bottom:80px}.lg\:sl-ml-20{margin-left:80px}.lg\:sl-mt-24{margin-top:96px}.lg\:sl-mr-24{margin-right:96px}.lg\:sl-mb-24{margin-bottom:96px}.lg\:sl-ml-24{margin-left:96px}.lg\:sl-mt-28{margin-top:112px}.lg\:sl-mr-28{margin-right:112px}.lg\:sl-mb-28{margin-bottom:112px}.lg\:sl-ml-28{margin-left:112px}.lg\:sl-mt-32{margin-top:128px}.lg\:sl-mr-32{margin-right:128px}.lg\:sl-mb-32{margin-bottom:128px}.lg\:sl-ml-32{margin-left:128px}.lg\:sl-mt-36{margin-top:144px}.lg\:sl-mr-36{margin-right:144px}.lg\:sl-mb-36{margin-bottom:144px}.lg\:sl-ml-36{margin-left:144px}.lg\:sl-mt-40{margin-top:160px}.lg\:sl-mr-40{margin-right:160px}.lg\:sl-mb-40{margin-bottom:160px}.lg\:sl-ml-40{margin-left:160px}.lg\:sl-mt-44{margin-top:176px}.lg\:sl-mr-44{margin-right:176px}.lg\:sl-mb-44{margin-bottom:176px}.lg\:sl-ml-44{margin-left:176px}.lg\:sl-mt-48{margin-top:192px}.lg\:sl-mr-48{margin-right:192px}.lg\:sl-mb-48{margin-bottom:192px}.lg\:sl-ml-48{margin-left:192px}.lg\:sl-mt-52{margin-top:208px}.lg\:sl-mr-52{margin-right:208px}.lg\:sl-mb-52{margin-bottom:208px}.lg\:sl-ml-52{margin-left:208px}.lg\:sl-mt-56{margin-top:224px}.lg\:sl-mr-56{margin-right:224px}.lg\:sl-mb-56{margin-bottom:224px}.lg\:sl-ml-56{margin-left:224px}.lg\:sl-mt-60{margin-top:240px}.lg\:sl-mr-60{margin-right:240px}.lg\:sl-mb-60{margin-bottom:240px}.lg\:sl-ml-60{margin-left:240px}.lg\:sl-mt-64{margin-top:256px}.lg\:sl-mr-64{margin-right:256px}.lg\:sl-mb-64{margin-bottom:256px}.lg\:sl-ml-64{margin-left:256px}.lg\:sl-mt-72{margin-top:288px}.lg\:sl-mr-72{margin-right:288px}.lg\:sl-mb-72{margin-bottom:288px}.lg\:sl-ml-72{margin-left:288px}.lg\:sl-mt-80{margin-top:320px}.lg\:sl-mr-80{margin-right:320px}.lg\:sl-mb-80{margin-bottom:320px}.lg\:sl-ml-80{margin-left:320px}.lg\:sl-mt-96{margin-top:384px}.lg\:sl-mr-96{margin-right:384px}.lg\:sl-mb-96{margin-bottom:384px}.lg\:sl-ml-96{margin-left:384px}.lg\:sl-mt-auto{margin-top:auto}.lg\:sl-mr-auto{margin-right:auto}.lg\:sl-mb-auto{margin-bottom:auto}.lg\:sl-ml-auto{margin-left:auto}.lg\:sl-mt-px{margin-top:1px}.lg\:sl-mr-px{margin-right:1px}.lg\:sl-mb-px{margin-bottom:1px}.lg\:sl-ml-px{margin-left:1px}.lg\:sl-mt-0\.5{margin-top:2px}.lg\:sl-mr-0\.5{margin-right:2px}.lg\:sl-mb-0\.5{margin-bottom:2px}.lg\:sl-ml-0\.5{margin-left:2px}.lg\:sl-mt-1\.5{margin-top:6px}.lg\:sl-mr-1\.5{margin-right:6px}.lg\:sl-mb-1\.5{margin-bottom:6px}.lg\:sl-ml-1\.5{margin-left:6px}.lg\:sl-mt-2\.5{margin-top:10px}.lg\:sl-mr-2\.5{margin-right:10px}.lg\:sl-mb-2\.5{margin-bottom:10px}.lg\:sl-ml-2\.5{margin-left:10px}.lg\:sl-mt-3\.5{margin-top:14px}.lg\:sl-mr-3\.5{margin-right:14px}.lg\:sl-mb-3\.5{margin-bottom:14px}.lg\:sl-ml-3\.5{margin-left:14px}.lg\:sl-mt-4\.5{margin-top:18px}.lg\:sl-mr-4\.5{margin-right:18px}.lg\:sl-mb-4\.5{margin-bottom:18px}.lg\:sl-ml-4\.5{margin-left:18px}.lg\:sl--mt-0{margin-top:0}.lg\:sl--mr-0{margin-right:0}.lg\:sl--mb-0{margin-bottom:0}.lg\:sl--ml-0{margin-left:0}.lg\:sl--mt-1{margin-top:-4px}.lg\:sl--mr-1{margin-right:-4px}.lg\:sl--mb-1{margin-bottom:-4px}.lg\:sl--ml-1{margin-left:-4px}.lg\:sl--mt-2{margin-top:-8px}.lg\:sl--mr-2{margin-right:-8px}.lg\:sl--mb-2{margin-bottom:-8px}.lg\:sl--ml-2{margin-left:-8px}.lg\:sl--mt-3{margin-top:-12px}.lg\:sl--mr-3{margin-right:-12px}.lg\:sl--mb-3{margin-bottom:-12px}.lg\:sl--ml-3{margin-left:-12px}.lg\:sl--mt-4{margin-top:-16px}.lg\:sl--mr-4{margin-right:-16px}.lg\:sl--mb-4{margin-bottom:-16px}.lg\:sl--ml-4{margin-left:-16px}.lg\:sl--mt-5{margin-top:-20px}.lg\:sl--mr-5{margin-right:-20px}.lg\:sl--mb-5{margin-bottom:-20px}.lg\:sl--ml-5{margin-left:-20px}.lg\:sl--mt-6{margin-top:-24px}.lg\:sl--mr-6{margin-right:-24px}.lg\:sl--mb-6{margin-bottom:-24px}.lg\:sl--ml-6{margin-left:-24px}.lg\:sl--mt-7{margin-top:-28px}.lg\:sl--mr-7{margin-right:-28px}.lg\:sl--mb-7{margin-bottom:-28px}.lg\:sl--ml-7{margin-left:-28px}.lg\:sl--mt-8{margin-top:-32px}.lg\:sl--mr-8{margin-right:-32px}.lg\:sl--mb-8{margin-bottom:-32px}.lg\:sl--ml-8{margin-left:-32px}.lg\:sl--mt-9{margin-top:-36px}.lg\:sl--mr-9{margin-right:-36px}.lg\:sl--mb-9{margin-bottom:-36px}.lg\:sl--ml-9{margin-left:-36px}.lg\:sl--mt-10{margin-top:-40px}.lg\:sl--mr-10{margin-right:-40px}.lg\:sl--mb-10{margin-bottom:-40px}.lg\:sl--ml-10{margin-left:-40px}.lg\:sl--mt-11{margin-top:-44px}.lg\:sl--mr-11{margin-right:-44px}.lg\:sl--mb-11{margin-bottom:-44px}.lg\:sl--ml-11{margin-left:-44px}.lg\:sl--mt-12{margin-top:-48px}.lg\:sl--mr-12{margin-right:-48px}.lg\:sl--mb-12{margin-bottom:-48px}.lg\:sl--ml-12{margin-left:-48px}.lg\:sl--mt-14{margin-top:-56px}.lg\:sl--mr-14{margin-right:-56px}.lg\:sl--mb-14{margin-bottom:-56px}.lg\:sl--ml-14{margin-left:-56px}.lg\:sl--mt-16{margin-top:-64px}.lg\:sl--mr-16{margin-right:-64px}.lg\:sl--mb-16{margin-bottom:-64px}.lg\:sl--ml-16{margin-left:-64px}.lg\:sl--mt-20{margin-top:-80px}.lg\:sl--mr-20{margin-right:-80px}.lg\:sl--mb-20{margin-bottom:-80px}.lg\:sl--ml-20{margin-left:-80px}.lg\:sl--mt-24{margin-top:-96px}.lg\:sl--mr-24{margin-right:-96px}.lg\:sl--mb-24{margin-bottom:-96px}.lg\:sl--ml-24{margin-left:-96px}.lg\:sl--mt-28{margin-top:-112px}.lg\:sl--mr-28{margin-right:-112px}.lg\:sl--mb-28{margin-bottom:-112px}.lg\:sl--ml-28{margin-left:-112px}.lg\:sl--mt-32{margin-top:-128px}.lg\:sl--mr-32{margin-right:-128px}.lg\:sl--mb-32{margin-bottom:-128px}.lg\:sl--ml-32{margin-left:-128px}.lg\:sl--mt-36{margin-top:-144px}.lg\:sl--mr-36{margin-right:-144px}.lg\:sl--mb-36{margin-bottom:-144px}.lg\:sl--ml-36{margin-left:-144px}.lg\:sl--mt-40{margin-top:-160px}.lg\:sl--mr-40{margin-right:-160px}.lg\:sl--mb-40{margin-bottom:-160px}.lg\:sl--ml-40{margin-left:-160px}.lg\:sl--mt-44{margin-top:-176px}.lg\:sl--mr-44{margin-right:-176px}.lg\:sl--mb-44{margin-bottom:-176px}.lg\:sl--ml-44{margin-left:-176px}.lg\:sl--mt-48{margin-top:-192px}.lg\:sl--mr-48{margin-right:-192px}.lg\:sl--mb-48{margin-bottom:-192px}.lg\:sl--ml-48{margin-left:-192px}.lg\:sl--mt-52{margin-top:-208px}.lg\:sl--mr-52{margin-right:-208px}.lg\:sl--mb-52{margin-bottom:-208px}.lg\:sl--ml-52{margin-left:-208px}.lg\:sl--mt-56{margin-top:-224px}.lg\:sl--mr-56{margin-right:-224px}.lg\:sl--mb-56{margin-bottom:-224px}.lg\:sl--ml-56{margin-left:-224px}.lg\:sl--mt-60{margin-top:-240px}.lg\:sl--mr-60{margin-right:-240px}.lg\:sl--mb-60{margin-bottom:-240px}.lg\:sl--ml-60{margin-left:-240px}.lg\:sl--mt-64{margin-top:-256px}.lg\:sl--mr-64{margin-right:-256px}.lg\:sl--mb-64{margin-bottom:-256px}.lg\:sl--ml-64{margin-left:-256px}.lg\:sl--mt-72{margin-top:-288px}.lg\:sl--mr-72{margin-right:-288px}.lg\:sl--mb-72{margin-bottom:-288px}.lg\:sl--ml-72{margin-left:-288px}.lg\:sl--mt-80{margin-top:-320px}.lg\:sl--mr-80{margin-right:-320px}.lg\:sl--mb-80{margin-bottom:-320px}.lg\:sl--ml-80{margin-left:-320px}.lg\:sl--mt-96{margin-top:-384px}.lg\:sl--mr-96{margin-right:-384px}.lg\:sl--mb-96{margin-bottom:-384px}.lg\:sl--ml-96{margin-left:-384px}.lg\:sl--mt-px{margin-top:-1px}.lg\:sl--mr-px{margin-right:-1px}.lg\:sl--mb-px{margin-bottom:-1px}.lg\:sl--ml-px{margin-left:-1px}.lg\:sl--mt-0\.5{margin-top:-2px}.lg\:sl--mr-0\.5{margin-right:-2px}.lg\:sl--mb-0\.5{margin-bottom:-2px}.lg\:sl--ml-0\.5{margin-left:-2px}.lg\:sl--mt-1\.5{margin-top:-6px}.lg\:sl--mr-1\.5{margin-right:-6px}.lg\:sl--mb-1\.5{margin-bottom:-6px}.lg\:sl--ml-1\.5{margin-left:-6px}.lg\:sl--mt-2\.5{margin-top:-10px}.lg\:sl--mr-2\.5{margin-right:-10px}.lg\:sl--mb-2\.5{margin-bottom:-10px}.lg\:sl--ml-2\.5{margin-left:-10px}.lg\:sl--mt-3\.5{margin-top:-14px}.lg\:sl--mr-3\.5{margin-right:-14px}.lg\:sl--mb-3\.5{margin-bottom:-14px}.lg\:sl--ml-3\.5{margin-left:-14px}.lg\:sl--mt-4\.5{margin-top:-18px}.lg\:sl--mr-4\.5{margin-right:-18px}.lg\:sl--mb-4\.5{margin-bottom:-18px}.lg\:sl--ml-4\.5{margin-left:-18px}.lg\:sl-max-h-full{max-height:100%}.lg\:sl-max-h-screen{max-height:100vh}.lg\:sl-max-w-none{max-width:none}.lg\:sl-max-w-full{max-width:100%}.lg\:sl-max-w-min{max-width:-moz-min-content;max-width:min-content}.lg\:sl-max-w-max{max-width:-moz-max-content;max-width:max-content}.lg\:sl-max-w-prose{max-width:65ch}.lg\:sl-min-h-full{min-height:100%}.lg\:sl-min-h-screen{min-height:100vh}.lg\:sl-min-w-full{min-width:100%}.lg\:sl-min-w-min{min-width:-moz-min-content;min-width:min-content}.lg\:sl-min-w-max{min-width:-moz-max-content;min-width:max-content}.lg\:sl-p-0{padding:0}.lg\:sl-p-1{padding:4px}.lg\:sl-p-2{padding:8px}.lg\:sl-p-3{padding:12px}.lg\:sl-p-4{padding:16px}.lg\:sl-p-5{padding:20px}.lg\:sl-p-6{padding:24px}.lg\:sl-p-7{padding:28px}.lg\:sl-p-8{padding:32px}.lg\:sl-p-9{padding:36px}.lg\:sl-p-10{padding:40px}.lg\:sl-p-11{padding:44px}.lg\:sl-p-12{padding:48px}.lg\:sl-p-14{padding:56px}.lg\:sl-p-16{padding:64px}.lg\:sl-p-20{padding:80px}.lg\:sl-p-24{padding:96px}.lg\:sl-p-28{padding:112px}.lg\:sl-p-32{padding:128px}.lg\:sl-p-36{padding:144px}.lg\:sl-p-40{padding:160px}.lg\:sl-p-44{padding:176px}.lg\:sl-p-48{padding:192px}.lg\:sl-p-52{padding:208px}.lg\:sl-p-56{padding:224px}.lg\:sl-p-60{padding:240px}.lg\:sl-p-64{padding:256px}.lg\:sl-p-72{padding:288px}.lg\:sl-p-80{padding:320px}.lg\:sl-p-96{padding:384px}.lg\:sl-p-px{padding:1px}.lg\:sl-p-0\.5{padding:2px}.lg\:sl-p-1\.5{padding:6px}.lg\:sl-p-2\.5{padding:10px}.lg\:sl-p-3\.5{padding:14px}.lg\:sl-p-4\.5{padding:18px}.lg\:sl-py-0{padding-bottom:0;padding-top:0}.lg\:sl-px-0{padding-left:0;padding-right:0}.lg\:sl-py-1{padding-bottom:4px;padding-top:4px}.lg\:sl-px-1{padding-left:4px;padding-right:4px}.lg\:sl-py-2{padding-bottom:8px;padding-top:8px}.lg\:sl-px-2{padding-left:8px;padding-right:8px}.lg\:sl-py-3{padding-bottom:12px;padding-top:12px}.lg\:sl-px-3{padding-left:12px;padding-right:12px}.lg\:sl-py-4{padding-bottom:16px;padding-top:16px}.lg\:sl-px-4{padding-left:16px;padding-right:16px}.lg\:sl-py-5{padding-bottom:20px;padding-top:20px}.lg\:sl-px-5{padding-left:20px;padding-right:20px}.lg\:sl-py-6{padding-bottom:24px;padding-top:24px}.lg\:sl-px-6{padding-left:24px;padding-right:24px}.lg\:sl-py-7{padding-bottom:28px;padding-top:28px}.lg\:sl-px-7{padding-left:28px;padding-right:28px}.lg\:sl-py-8{padding-bottom:32px;padding-top:32px}.lg\:sl-px-8{padding-left:32px;padding-right:32px}.lg\:sl-py-9{padding-bottom:36px;padding-top:36px}.lg\:sl-px-9{padding-left:36px;padding-right:36px}.lg\:sl-py-10{padding-bottom:40px;padding-top:40px}.lg\:sl-px-10{padding-left:40px;padding-right:40px}.lg\:sl-py-11{padding-bottom:44px;padding-top:44px}.lg\:sl-px-11{padding-left:44px;padding-right:44px}.lg\:sl-py-12{padding-bottom:48px;padding-top:48px}.lg\:sl-px-12{padding-left:48px;padding-right:48px}.lg\:sl-py-14{padding-bottom:56px;padding-top:56px}.lg\:sl-px-14{padding-left:56px;padding-right:56px}.lg\:sl-py-16{padding-bottom:64px;padding-top:64px}.lg\:sl-px-16{padding-left:64px;padding-right:64px}.lg\:sl-py-20{padding-bottom:80px;padding-top:80px}.lg\:sl-px-20{padding-left:80px;padding-right:80px}.lg\:sl-py-24{padding-bottom:96px;padding-top:96px}.lg\:sl-px-24{padding-left:96px;padding-right:96px}.lg\:sl-py-28{padding-bottom:112px;padding-top:112px}.lg\:sl-px-28{padding-left:112px;padding-right:112px}.lg\:sl-py-32{padding-bottom:128px;padding-top:128px}.lg\:sl-px-32{padding-left:128px;padding-right:128px}.lg\:sl-py-36{padding-bottom:144px;padding-top:144px}.lg\:sl-px-36{padding-left:144px;padding-right:144px}.lg\:sl-py-40{padding-bottom:160px;padding-top:160px}.lg\:sl-px-40{padding-left:160px;padding-right:160px}.lg\:sl-py-44{padding-bottom:176px;padding-top:176px}.lg\:sl-px-44{padding-left:176px;padding-right:176px}.lg\:sl-py-48{padding-bottom:192px;padding-top:192px}.lg\:sl-px-48{padding-left:192px;padding-right:192px}.lg\:sl-py-52{padding-bottom:208px;padding-top:208px}.lg\:sl-px-52{padding-left:208px;padding-right:208px}.lg\:sl-py-56{padding-bottom:224px;padding-top:224px}.lg\:sl-px-56{padding-left:224px;padding-right:224px}.lg\:sl-py-60{padding-bottom:240px;padding-top:240px}.lg\:sl-px-60{padding-left:240px;padding-right:240px}.lg\:sl-py-64{padding-bottom:256px;padding-top:256px}.lg\:sl-px-64{padding-left:256px;padding-right:256px}.lg\:sl-py-72{padding-bottom:288px;padding-top:288px}.lg\:sl-px-72{padding-left:288px;padding-right:288px}.lg\:sl-py-80{padding-bottom:320px;padding-top:320px}.lg\:sl-px-80{padding-left:320px;padding-right:320px}.lg\:sl-py-96{padding-bottom:384px;padding-top:384px}.lg\:sl-px-96{padding-left:384px;padding-right:384px}.lg\:sl-py-px{padding-bottom:1px;padding-top:1px}.lg\:sl-px-px{padding-left:1px;padding-right:1px}.lg\:sl-py-0\.5{padding-bottom:2px;padding-top:2px}.lg\:sl-px-0\.5{padding-left:2px;padding-right:2px}.lg\:sl-py-1\.5{padding-bottom:6px;padding-top:6px}.lg\:sl-px-1\.5{padding-left:6px;padding-right:6px}.lg\:sl-py-2\.5{padding-bottom:10px;padding-top:10px}.lg\:sl-px-2\.5{padding-left:10px;padding-right:10px}.lg\:sl-py-3\.5{padding-bottom:14px;padding-top:14px}.lg\:sl-px-3\.5{padding-left:14px;padding-right:14px}.lg\:sl-py-4\.5{padding-bottom:18px;padding-top:18px}.lg\:sl-px-4\.5{padding-left:18px;padding-right:18px}.lg\:sl-pt-0{padding-top:0}.lg\:sl-pr-0{padding-right:0}.lg\:sl-pb-0{padding-bottom:0}.lg\:sl-pl-0{padding-left:0}.lg\:sl-pt-1{padding-top:4px}.lg\:sl-pr-1{padding-right:4px}.lg\:sl-pb-1{padding-bottom:4px}.lg\:sl-pl-1{padding-left:4px}.lg\:sl-pt-2{padding-top:8px}.lg\:sl-pr-2{padding-right:8px}.lg\:sl-pb-2{padding-bottom:8px}.lg\:sl-pl-2{padding-left:8px}.lg\:sl-pt-3{padding-top:12px}.lg\:sl-pr-3{padding-right:12px}.lg\:sl-pb-3{padding-bottom:12px}.lg\:sl-pl-3{padding-left:12px}.lg\:sl-pt-4{padding-top:16px}.lg\:sl-pr-4{padding-right:16px}.lg\:sl-pb-4{padding-bottom:16px}.lg\:sl-pl-4{padding-left:16px}.lg\:sl-pt-5{padding-top:20px}.lg\:sl-pr-5{padding-right:20px}.lg\:sl-pb-5{padding-bottom:20px}.lg\:sl-pl-5{padding-left:20px}.lg\:sl-pt-6{padding-top:24px}.lg\:sl-pr-6{padding-right:24px}.lg\:sl-pb-6{padding-bottom:24px}.lg\:sl-pl-6{padding-left:24px}.lg\:sl-pt-7{padding-top:28px}.lg\:sl-pr-7{padding-right:28px}.lg\:sl-pb-7{padding-bottom:28px}.lg\:sl-pl-7{padding-left:28px}.lg\:sl-pt-8{padding-top:32px}.lg\:sl-pr-8{padding-right:32px}.lg\:sl-pb-8{padding-bottom:32px}.lg\:sl-pl-8{padding-left:32px}.lg\:sl-pt-9{padding-top:36px}.lg\:sl-pr-9{padding-right:36px}.lg\:sl-pb-9{padding-bottom:36px}.lg\:sl-pl-9{padding-left:36px}.lg\:sl-pt-10{padding-top:40px}.lg\:sl-pr-10{padding-right:40px}.lg\:sl-pb-10{padding-bottom:40px}.lg\:sl-pl-10{padding-left:40px}.lg\:sl-pt-11{padding-top:44px}.lg\:sl-pr-11{padding-right:44px}.lg\:sl-pb-11{padding-bottom:44px}.lg\:sl-pl-11{padding-left:44px}.lg\:sl-pt-12{padding-top:48px}.lg\:sl-pr-12{padding-right:48px}.lg\:sl-pb-12{padding-bottom:48px}.lg\:sl-pl-12{padding-left:48px}.lg\:sl-pt-14{padding-top:56px}.lg\:sl-pr-14{padding-right:56px}.lg\:sl-pb-14{padding-bottom:56px}.lg\:sl-pl-14{padding-left:56px}.lg\:sl-pt-16{padding-top:64px}.lg\:sl-pr-16{padding-right:64px}.lg\:sl-pb-16{padding-bottom:64px}.lg\:sl-pl-16{padding-left:64px}.lg\:sl-pt-20{padding-top:80px}.lg\:sl-pr-20{padding-right:80px}.lg\:sl-pb-20{padding-bottom:80px}.lg\:sl-pl-20{padding-left:80px}.lg\:sl-pt-24{padding-top:96px}.lg\:sl-pr-24{padding-right:96px}.lg\:sl-pb-24{padding-bottom:96px}.lg\:sl-pl-24{padding-left:96px}.lg\:sl-pt-28{padding-top:112px}.lg\:sl-pr-28{padding-right:112px}.lg\:sl-pb-28{padding-bottom:112px}.lg\:sl-pl-28{padding-left:112px}.lg\:sl-pt-32{padding-top:128px}.lg\:sl-pr-32{padding-right:128px}.lg\:sl-pb-32{padding-bottom:128px}.lg\:sl-pl-32{padding-left:128px}.lg\:sl-pt-36{padding-top:144px}.lg\:sl-pr-36{padding-right:144px}.lg\:sl-pb-36{padding-bottom:144px}.lg\:sl-pl-36{padding-left:144px}.lg\:sl-pt-40{padding-top:160px}.lg\:sl-pr-40{padding-right:160px}.lg\:sl-pb-40{padding-bottom:160px}.lg\:sl-pl-40{padding-left:160px}.lg\:sl-pt-44{padding-top:176px}.lg\:sl-pr-44{padding-right:176px}.lg\:sl-pb-44{padding-bottom:176px}.lg\:sl-pl-44{padding-left:176px}.lg\:sl-pt-48{padding-top:192px}.lg\:sl-pr-48{padding-right:192px}.lg\:sl-pb-48{padding-bottom:192px}.lg\:sl-pl-48{padding-left:192px}.lg\:sl-pt-52{padding-top:208px}.lg\:sl-pr-52{padding-right:208px}.lg\:sl-pb-52{padding-bottom:208px}.lg\:sl-pl-52{padding-left:208px}.lg\:sl-pt-56{padding-top:224px}.lg\:sl-pr-56{padding-right:224px}.lg\:sl-pb-56{padding-bottom:224px}.lg\:sl-pl-56{padding-left:224px}.lg\:sl-pt-60{padding-top:240px}.lg\:sl-pr-60{padding-right:240px}.lg\:sl-pb-60{padding-bottom:240px}.lg\:sl-pl-60{padding-left:240px}.lg\:sl-pt-64{padding-top:256px}.lg\:sl-pr-64{padding-right:256px}.lg\:sl-pb-64{padding-bottom:256px}.lg\:sl-pl-64{padding-left:256px}.lg\:sl-pt-72{padding-top:288px}.lg\:sl-pr-72{padding-right:288px}.lg\:sl-pb-72{padding-bottom:288px}.lg\:sl-pl-72{padding-left:288px}.lg\:sl-pt-80{padding-top:320px}.lg\:sl-pr-80{padding-right:320px}.lg\:sl-pb-80{padding-bottom:320px}.lg\:sl-pl-80{padding-left:320px}.lg\:sl-pt-96{padding-top:384px}.lg\:sl-pr-96{padding-right:384px}.lg\:sl-pb-96{padding-bottom:384px}.lg\:sl-pl-96{padding-left:384px}.lg\:sl-pt-px{padding-top:1px}.lg\:sl-pr-px{padding-right:1px}.lg\:sl-pb-px{padding-bottom:1px}.lg\:sl-pl-px{padding-left:1px}.lg\:sl-pt-0\.5{padding-top:2px}.lg\:sl-pr-0\.5{padding-right:2px}.lg\:sl-pb-0\.5{padding-bottom:2px}.lg\:sl-pl-0\.5{padding-left:2px}.lg\:sl-pt-1\.5{padding-top:6px}.lg\:sl-pr-1\.5{padding-right:6px}.lg\:sl-pb-1\.5{padding-bottom:6px}.lg\:sl-pl-1\.5{padding-left:6px}.lg\:sl-pt-2\.5{padding-top:10px}.lg\:sl-pr-2\.5{padding-right:10px}.lg\:sl-pb-2\.5{padding-bottom:10px}.lg\:sl-pl-2\.5{padding-left:10px}.lg\:sl-pt-3\.5{padding-top:14px}.lg\:sl-pr-3\.5{padding-right:14px}.lg\:sl-pb-3\.5{padding-bottom:14px}.lg\:sl-pl-3\.5{padding-left:14px}.lg\:sl-pt-4\.5{padding-top:18px}.lg\:sl-pr-4\.5{padding-right:18px}.lg\:sl-pb-4\.5{padding-bottom:18px}.lg\:sl-pl-4\.5{padding-left:18px}.lg\:sl-static{position:static}.lg\:sl-fixed{position:fixed}.lg\:sl-absolute{position:absolute}.lg\:sl-relative{position:relative}.lg\:sl-sticky{position:-webkit-sticky;position:sticky}.lg\:sl-visible{visibility:visible}.lg\:sl-invisible{visibility:hidden}.sl-group:hover .lg\:group-hover\:sl-visible{visibility:visible}.sl-group:hover .lg\:group-hover\:sl-invisible{visibility:hidden}.sl-group:focus .lg\:group-focus\:sl-visible{visibility:visible}.sl-group:focus .lg\:group-focus\:sl-invisible{visibility:hidden}.lg\:sl-w-0{width:0}.lg\:sl-w-1{width:4px}.lg\:sl-w-2{width:8px}.lg\:sl-w-3{width:12px}.lg\:sl-w-4{width:16px}.lg\:sl-w-5{width:20px}.lg\:sl-w-6{width:24px}.lg\:sl-w-7{width:28px}.lg\:sl-w-8{width:32px}.lg\:sl-w-9{width:36px}.lg\:sl-w-10{width:40px}.lg\:sl-w-11{width:44px}.lg\:sl-w-12{width:48px}.lg\:sl-w-14{width:56px}.lg\:sl-w-16{width:64px}.lg\:sl-w-20{width:80px}.lg\:sl-w-24{width:96px}.lg\:sl-w-28{width:112px}.lg\:sl-w-32{width:128px}.lg\:sl-w-36{width:144px}.lg\:sl-w-40{width:160px}.lg\:sl-w-44{width:176px}.lg\:sl-w-48{width:192px}.lg\:sl-w-52{width:208px}.lg\:sl-w-56{width:224px}.lg\:sl-w-60{width:240px}.lg\:sl-w-64{width:256px}.lg\:sl-w-72{width:288px}.lg\:sl-w-80{width:320px}.lg\:sl-w-96{width:384px}.lg\:sl-w-auto{width:auto}.lg\:sl-w-px{width:1px}.lg\:sl-w-0\.5{width:2px}.lg\:sl-w-1\.5{width:6px}.lg\:sl-w-2\.5{width:10px}.lg\:sl-w-3\.5{width:14px}.lg\:sl-w-4\.5{width:18px}.lg\:sl-w-xs{width:20px}.lg\:sl-w-sm{width:24px}.lg\:sl-w-md{width:32px}.lg\:sl-w-lg{width:36px}.lg\:sl-w-xl{width:44px}.lg\:sl-w-2xl{width:52px}.lg\:sl-w-3xl{width:60px}.lg\:sl-w-1\/2{width:50%}.lg\:sl-w-1\/3{width:33.333333%}.lg\:sl-w-2\/3{width:66.666667%}.lg\:sl-w-1\/4{width:25%}.lg\:sl-w-2\/4{width:50%}.lg\:sl-w-3\/4{width:75%}.lg\:sl-w-1\/5{width:20%}.lg\:sl-w-2\/5{width:40%}.lg\:sl-w-3\/5{width:60%}.lg\:sl-w-4\/5{width:80%}.lg\:sl-w-1\/6{width:16.666667%}.lg\:sl-w-2\/6{width:33.333333%}.lg\:sl-w-3\/6{width:50%}.lg\:sl-w-4\/6{width:66.666667%}.lg\:sl-w-5\/6{width:83.333333%}.lg\:sl-w-full{width:100%}.lg\:sl-w-screen{width:100vw}.lg\:sl-w-min{width:-moz-min-content;width:min-content}.lg\:sl-w-max{width:-moz-max-content;width:max-content}}@media (max-width:1399px){.xl\:sl-stack--1{gap:4px}.xl\:sl-stack--2{gap:8px}.xl\:sl-stack--3{gap:12px}.xl\:sl-stack--4{gap:16px}.xl\:sl-stack--5{gap:20px}.xl\:sl-stack--6{gap:24px}.xl\:sl-stack--7{gap:28px}.xl\:sl-stack--8{gap:32px}.xl\:sl-stack--9{gap:36px}.xl\:sl-stack--10{gap:40px}.xl\:sl-stack--12{gap:48px}.xl\:sl-stack--14{gap:56px}.xl\:sl-stack--16{gap:64px}.xl\:sl-stack--20{gap:80px}.xl\:sl-stack--24{gap:96px}.xl\:sl-stack--32{gap:128px}.xl\:sl-content-center{align-content:center}.xl\:sl-content-start{align-content:flex-start}.xl\:sl-content-end{align-content:flex-end}.xl\:sl-content-between{align-content:space-between}.xl\:sl-content-around{align-content:space-around}.xl\:sl-content-evenly{align-content:space-evenly}.xl\:sl-items-start{align-items:flex-start}.xl\:sl-items-end{align-items:flex-end}.xl\:sl-items-center{align-items:center}.xl\:sl-items-baseline{align-items:baseline}.xl\:sl-items-stretch{align-items:stretch}.xl\:sl-self-auto{align-self:auto}.xl\:sl-self-start{align-self:flex-start}.xl\:sl-self-end{align-self:flex-end}.xl\:sl-self-center{align-self:center}.xl\:sl-self-stretch{align-self:stretch}.xl\:sl-blur-0,.xl\:sl-blur-none{--tw-blur:blur(0)}.xl\:sl-blur-sm{--tw-blur:blur(4px)}.xl\:sl-blur{--tw-blur:blur(8px)}.xl\:sl-blur-md{--tw-blur:blur(12px)}.xl\:sl-blur-lg{--tw-blur:blur(16px)}.xl\:sl-blur-xl{--tw-blur:blur(24px)}.xl\:sl-blur-2xl{--tw-blur:blur(40px)}.xl\:sl-blur-3xl{--tw-blur:blur(64px)}.xl\:sl-block{display:block}.xl\:sl-inline-block{display:inline-block}.xl\:sl-inline{display:inline}.xl\:sl-flex{display:flex}.xl\:sl-inline-flex{display:inline-flex}.xl\:sl-table{display:table}.xl\:sl-inline-table{display:inline-table}.xl\:sl-table-caption{display:table-caption}.xl\:sl-table-cell{display:table-cell}.xl\:sl-table-column{display:table-column}.xl\:sl-table-column-group{display:table-column-group}.xl\:sl-table-footer-group{display:table-footer-group}.xl\:sl-table-header-group{display:table-header-group}.xl\:sl-table-row-group{display:table-row-group}.xl\:sl-table-row{display:table-row}.xl\:sl-flow-root{display:flow-root}.xl\:sl-grid{display:grid}.xl\:sl-inline-grid{display:inline-grid}.xl\:sl-contents{display:contents}.xl\:sl-list-item{display:list-item}.xl\:sl-hidden{display:none}.xl\:sl-drop-shadow{--tw-drop-shadow:drop-shadow(var(--drop-shadow-default1)) drop-shadow(var(--drop-shadow-default2))}.xl\:sl-flex-1{flex:1 1}.xl\:sl-flex-auto{flex:1 1 auto}.xl\:sl-flex-initial{flex:0 1 auto}.xl\:sl-flex-none{flex:none}.xl\:sl-flex-row{flex-direction:row}.xl\:sl-flex-row-reverse{flex-direction:row-reverse}.xl\:sl-flex-col{flex-direction:column}.xl\:sl-flex-col-reverse{flex-direction:column-reverse}.xl\:sl-flex-grow-0{flex-grow:0}.xl\:sl-flex-grow{flex-grow:1}.xl\:sl-flex-shrink-0{flex-shrink:0}.xl\:sl-flex-shrink{flex-shrink:1}.xl\:sl-flex-wrap{flex-wrap:wrap}.xl\:sl-flex-wrap-reverse{flex-wrap:wrap-reverse}.xl\:sl-flex-nowrap{flex-wrap:nowrap}.xl\:sl-h-0{height:0}.xl\:sl-h-1{height:4px}.xl\:sl-h-2{height:8px}.xl\:sl-h-3{height:12px}.xl\:sl-h-4{height:16px}.xl\:sl-h-5{height:20px}.xl\:sl-h-6{height:24px}.xl\:sl-h-7{height:28px}.xl\:sl-h-8{height:32px}.xl\:sl-h-9{height:36px}.xl\:sl-h-10{height:40px}.xl\:sl-h-11{height:44px}.xl\:sl-h-12{height:48px}.xl\:sl-h-14{height:56px}.xl\:sl-h-16{height:64px}.xl\:sl-h-20{height:80px}.xl\:sl-h-24{height:96px}.xl\:sl-h-28{height:112px}.xl\:sl-h-32{height:128px}.xl\:sl-h-36{height:144px}.xl\:sl-h-40{height:160px}.xl\:sl-h-44{height:176px}.xl\:sl-h-48{height:192px}.xl\:sl-h-52{height:208px}.xl\:sl-h-56{height:224px}.xl\:sl-h-60{height:240px}.xl\:sl-h-64{height:256px}.xl\:sl-h-72{height:288px}.xl\:sl-h-80{height:320px}.xl\:sl-h-96{height:384px}.xl\:sl-h-auto{height:auto}.xl\:sl-h-px{height:1px}.xl\:sl-h-0\.5{height:2px}.xl\:sl-h-1\.5{height:6px}.xl\:sl-h-2\.5{height:10px}.xl\:sl-h-3\.5{height:14px}.xl\:sl-h-4\.5{height:18px}.xl\:sl-h-xs{height:20px}.xl\:sl-h-sm{height:24px}.xl\:sl-h-md{height:32px}.xl\:sl-h-lg{height:36px}.xl\:sl-h-xl{height:44px}.xl\:sl-h-2xl{height:52px}.xl\:sl-h-3xl{height:60px}.xl\:sl-h-full{height:100%}.xl\:sl-h-screen{height:100vh}.xl\:sl-justify-start{justify-content:flex-start}.xl\:sl-justify-end{justify-content:flex-end}.xl\:sl-justify-center{justify-content:center}.xl\:sl-justify-between{justify-content:space-between}.xl\:sl-justify-around{justify-content:space-around}.xl\:sl-justify-evenly{justify-content:space-evenly}.xl\:sl-justify-items-start{justify-items:start}.xl\:sl-justify-items-end{justify-items:end}.xl\:sl-justify-items-center{justify-items:center}.xl\:sl-justify-items-stretch{justify-items:stretch}.xl\:sl-justify-self-auto{justify-self:auto}.xl\:sl-justify-self-start{justify-self:start}.xl\:sl-justify-self-end{justify-self:end}.xl\:sl-justify-self-center{justify-self:center}.xl\:sl-justify-self-stretch{justify-self:stretch}.xl\:sl-m-0{margin:0}.xl\:sl-m-1{margin:4px}.xl\:sl-m-2{margin:8px}.xl\:sl-m-3{margin:12px}.xl\:sl-m-4{margin:16px}.xl\:sl-m-5{margin:20px}.xl\:sl-m-6{margin:24px}.xl\:sl-m-7{margin:28px}.xl\:sl-m-8{margin:32px}.xl\:sl-m-9{margin:36px}.xl\:sl-m-10{margin:40px}.xl\:sl-m-11{margin:44px}.xl\:sl-m-12{margin:48px}.xl\:sl-m-14{margin:56px}.xl\:sl-m-16{margin:64px}.xl\:sl-m-20{margin:80px}.xl\:sl-m-24{margin:96px}.xl\:sl-m-28{margin:112px}.xl\:sl-m-32{margin:128px}.xl\:sl-m-36{margin:144px}.xl\:sl-m-40{margin:160px}.xl\:sl-m-44{margin:176px}.xl\:sl-m-48{margin:192px}.xl\:sl-m-52{margin:208px}.xl\:sl-m-56{margin:224px}.xl\:sl-m-60{margin:240px}.xl\:sl-m-64{margin:256px}.xl\:sl-m-72{margin:288px}.xl\:sl-m-80{margin:320px}.xl\:sl-m-96{margin:384px}.xl\:sl-m-auto{margin:auto}.xl\:sl-m-px{margin:1px}.xl\:sl-m-0\.5{margin:2px}.xl\:sl-m-1\.5{margin:6px}.xl\:sl-m-2\.5{margin:10px}.xl\:sl-m-3\.5{margin:14px}.xl\:sl-m-4\.5{margin:18px}.xl\:sl--m-0{margin:0}.xl\:sl--m-1{margin:-4px}.xl\:sl--m-2{margin:-8px}.xl\:sl--m-3{margin:-12px}.xl\:sl--m-4{margin:-16px}.xl\:sl--m-5{margin:-20px}.xl\:sl--m-6{margin:-24px}.xl\:sl--m-7{margin:-28px}.xl\:sl--m-8{margin:-32px}.xl\:sl--m-9{margin:-36px}.xl\:sl--m-10{margin:-40px}.xl\:sl--m-11{margin:-44px}.xl\:sl--m-12{margin:-48px}.xl\:sl--m-14{margin:-56px}.xl\:sl--m-16{margin:-64px}.xl\:sl--m-20{margin:-80px}.xl\:sl--m-24{margin:-96px}.xl\:sl--m-28{margin:-112px}.xl\:sl--m-32{margin:-128px}.xl\:sl--m-36{margin:-144px}.xl\:sl--m-40{margin:-160px}.xl\:sl--m-44{margin:-176px}.xl\:sl--m-48{margin:-192px}.xl\:sl--m-52{margin:-208px}.xl\:sl--m-56{margin:-224px}.xl\:sl--m-60{margin:-240px}.xl\:sl--m-64{margin:-256px}.xl\:sl--m-72{margin:-288px}.xl\:sl--m-80{margin:-320px}.xl\:sl--m-96{margin:-384px}.xl\:sl--m-px{margin:-1px}.xl\:sl--m-0\.5{margin:-2px}.xl\:sl--m-1\.5{margin:-6px}.xl\:sl--m-2\.5{margin:-10px}.xl\:sl--m-3\.5{margin:-14px}.xl\:sl--m-4\.5{margin:-18px}.xl\:sl-my-0{margin-bottom:0;margin-top:0}.xl\:sl-mx-0{margin-left:0;margin-right:0}.xl\:sl-my-1{margin-bottom:4px;margin-top:4px}.xl\:sl-mx-1{margin-left:4px;margin-right:4px}.xl\:sl-my-2{margin-bottom:8px;margin-top:8px}.xl\:sl-mx-2{margin-left:8px;margin-right:8px}.xl\:sl-my-3{margin-bottom:12px;margin-top:12px}.xl\:sl-mx-3{margin-left:12px;margin-right:12px}.xl\:sl-my-4{margin-bottom:16px;margin-top:16px}.xl\:sl-mx-4{margin-left:16px;margin-right:16px}.xl\:sl-my-5{margin-bottom:20px;margin-top:20px}.xl\:sl-mx-5{margin-left:20px;margin-right:20px}.xl\:sl-my-6{margin-bottom:24px;margin-top:24px}.xl\:sl-mx-6{margin-left:24px;margin-right:24px}.xl\:sl-my-7{margin-bottom:28px;margin-top:28px}.xl\:sl-mx-7{margin-left:28px;margin-right:28px}.xl\:sl-my-8{margin-bottom:32px;margin-top:32px}.xl\:sl-mx-8{margin-left:32px;margin-right:32px}.xl\:sl-my-9{margin-bottom:36px;margin-top:36px}.xl\:sl-mx-9{margin-left:36px;margin-right:36px}.xl\:sl-my-10{margin-bottom:40px;margin-top:40px}.xl\:sl-mx-10{margin-left:40px;margin-right:40px}.xl\:sl-my-11{margin-bottom:44px;margin-top:44px}.xl\:sl-mx-11{margin-left:44px;margin-right:44px}.xl\:sl-my-12{margin-bottom:48px;margin-top:48px}.xl\:sl-mx-12{margin-left:48px;margin-right:48px}.xl\:sl-my-14{margin-bottom:56px;margin-top:56px}.xl\:sl-mx-14{margin-left:56px;margin-right:56px}.xl\:sl-my-16{margin-bottom:64px;margin-top:64px}.xl\:sl-mx-16{margin-left:64px;margin-right:64px}.xl\:sl-my-20{margin-bottom:80px;margin-top:80px}.xl\:sl-mx-20{margin-left:80px;margin-right:80px}.xl\:sl-my-24{margin-bottom:96px;margin-top:96px}.xl\:sl-mx-24{margin-left:96px;margin-right:96px}.xl\:sl-my-28{margin-bottom:112px;margin-top:112px}.xl\:sl-mx-28{margin-left:112px;margin-right:112px}.xl\:sl-my-32{margin-bottom:128px;margin-top:128px}.xl\:sl-mx-32{margin-left:128px;margin-right:128px}.xl\:sl-my-36{margin-bottom:144px;margin-top:144px}.xl\:sl-mx-36{margin-left:144px;margin-right:144px}.xl\:sl-my-40{margin-bottom:160px;margin-top:160px}.xl\:sl-mx-40{margin-left:160px;margin-right:160px}.xl\:sl-my-44{margin-bottom:176px;margin-top:176px}.xl\:sl-mx-44{margin-left:176px;margin-right:176px}.xl\:sl-my-48{margin-bottom:192px;margin-top:192px}.xl\:sl-mx-48{margin-left:192px;margin-right:192px}.xl\:sl-my-52{margin-bottom:208px;margin-top:208px}.xl\:sl-mx-52{margin-left:208px;margin-right:208px}.xl\:sl-my-56{margin-bottom:224px;margin-top:224px}.xl\:sl-mx-56{margin-left:224px;margin-right:224px}.xl\:sl-my-60{margin-bottom:240px;margin-top:240px}.xl\:sl-mx-60{margin-left:240px;margin-right:240px}.xl\:sl-my-64{margin-bottom:256px;margin-top:256px}.xl\:sl-mx-64{margin-left:256px;margin-right:256px}.xl\:sl-my-72{margin-bottom:288px;margin-top:288px}.xl\:sl-mx-72{margin-left:288px;margin-right:288px}.xl\:sl-my-80{margin-bottom:320px;margin-top:320px}.xl\:sl-mx-80{margin-left:320px;margin-right:320px}.xl\:sl-my-96{margin-bottom:384px;margin-top:384px}.xl\:sl-mx-96{margin-left:384px;margin-right:384px}.xl\:sl-my-auto{margin-bottom:auto;margin-top:auto}.xl\:sl-mx-auto{margin-left:auto;margin-right:auto}.xl\:sl-my-px{margin-bottom:1px;margin-top:1px}.xl\:sl-mx-px{margin-left:1px;margin-right:1px}.xl\:sl-my-0\.5{margin-bottom:2px;margin-top:2px}.xl\:sl-mx-0\.5{margin-left:2px;margin-right:2px}.xl\:sl-my-1\.5{margin-bottom:6px;margin-top:6px}.xl\:sl-mx-1\.5{margin-left:6px;margin-right:6px}.xl\:sl-my-2\.5{margin-bottom:10px;margin-top:10px}.xl\:sl-mx-2\.5{margin-left:10px;margin-right:10px}.xl\:sl-my-3\.5{margin-bottom:14px;margin-top:14px}.xl\:sl-mx-3\.5{margin-left:14px;margin-right:14px}.xl\:sl-my-4\.5{margin-bottom:18px;margin-top:18px}.xl\:sl-mx-4\.5{margin-left:18px;margin-right:18px}.xl\:sl--my-0{margin-bottom:0;margin-top:0}.xl\:sl--mx-0{margin-left:0;margin-right:0}.xl\:sl--my-1{margin-bottom:-4px;margin-top:-4px}.xl\:sl--mx-1{margin-left:-4px;margin-right:-4px}.xl\:sl--my-2{margin-bottom:-8px;margin-top:-8px}.xl\:sl--mx-2{margin-left:-8px;margin-right:-8px}.xl\:sl--my-3{margin-bottom:-12px;margin-top:-12px}.xl\:sl--mx-3{margin-left:-12px;margin-right:-12px}.xl\:sl--my-4{margin-bottom:-16px;margin-top:-16px}.xl\:sl--mx-4{margin-left:-16px;margin-right:-16px}.xl\:sl--my-5{margin-bottom:-20px;margin-top:-20px}.xl\:sl--mx-5{margin-left:-20px;margin-right:-20px}.xl\:sl--my-6{margin-bottom:-24px;margin-top:-24px}.xl\:sl--mx-6{margin-left:-24px;margin-right:-24px}.xl\:sl--my-7{margin-bottom:-28px;margin-top:-28px}.xl\:sl--mx-7{margin-left:-28px;margin-right:-28px}.xl\:sl--my-8{margin-bottom:-32px;margin-top:-32px}.xl\:sl--mx-8{margin-left:-32px;margin-right:-32px}.xl\:sl--my-9{margin-bottom:-36px;margin-top:-36px}.xl\:sl--mx-9{margin-left:-36px;margin-right:-36px}.xl\:sl--my-10{margin-bottom:-40px;margin-top:-40px}.xl\:sl--mx-10{margin-left:-40px;margin-right:-40px}.xl\:sl--my-11{margin-bottom:-44px;margin-top:-44px}.xl\:sl--mx-11{margin-left:-44px;margin-right:-44px}.xl\:sl--my-12{margin-bottom:-48px;margin-top:-48px}.xl\:sl--mx-12{margin-left:-48px;margin-right:-48px}.xl\:sl--my-14{margin-bottom:-56px;margin-top:-56px}.xl\:sl--mx-14{margin-left:-56px;margin-right:-56px}.xl\:sl--my-16{margin-bottom:-64px;margin-top:-64px}.xl\:sl--mx-16{margin-left:-64px;margin-right:-64px}.xl\:sl--my-20{margin-bottom:-80px;margin-top:-80px}.xl\:sl--mx-20{margin-left:-80px;margin-right:-80px}.xl\:sl--my-24{margin-bottom:-96px;margin-top:-96px}.xl\:sl--mx-24{margin-left:-96px;margin-right:-96px}.xl\:sl--my-28{margin-bottom:-112px;margin-top:-112px}.xl\:sl--mx-28{margin-left:-112px;margin-right:-112px}.xl\:sl--my-32{margin-bottom:-128px;margin-top:-128px}.xl\:sl--mx-32{margin-left:-128px;margin-right:-128px}.xl\:sl--my-36{margin-bottom:-144px;margin-top:-144px}.xl\:sl--mx-36{margin-left:-144px;margin-right:-144px}.xl\:sl--my-40{margin-bottom:-160px;margin-top:-160px}.xl\:sl--mx-40{margin-left:-160px;margin-right:-160px}.xl\:sl--my-44{margin-bottom:-176px;margin-top:-176px}.xl\:sl--mx-44{margin-left:-176px;margin-right:-176px}.xl\:sl--my-48{margin-bottom:-192px;margin-top:-192px}.xl\:sl--mx-48{margin-left:-192px;margin-right:-192px}.xl\:sl--my-52{margin-bottom:-208px;margin-top:-208px}.xl\:sl--mx-52{margin-left:-208px;margin-right:-208px}.xl\:sl--my-56{margin-bottom:-224px;margin-top:-224px}.xl\:sl--mx-56{margin-left:-224px;margin-right:-224px}.xl\:sl--my-60{margin-bottom:-240px;margin-top:-240px}.xl\:sl--mx-60{margin-left:-240px;margin-right:-240px}.xl\:sl--my-64{margin-bottom:-256px;margin-top:-256px}.xl\:sl--mx-64{margin-left:-256px;margin-right:-256px}.xl\:sl--my-72{margin-bottom:-288px;margin-top:-288px}.xl\:sl--mx-72{margin-left:-288px;margin-right:-288px}.xl\:sl--my-80{margin-bottom:-320px;margin-top:-320px}.xl\:sl--mx-80{margin-left:-320px;margin-right:-320px}.xl\:sl--my-96{margin-bottom:-384px;margin-top:-384px}.xl\:sl--mx-96{margin-left:-384px;margin-right:-384px}.xl\:sl--my-px{margin-bottom:-1px;margin-top:-1px}.xl\:sl--mx-px{margin-left:-1px;margin-right:-1px}.xl\:sl--my-0\.5{margin-bottom:-2px;margin-top:-2px}.xl\:sl--mx-0\.5{margin-left:-2px;margin-right:-2px}.xl\:sl--my-1\.5{margin-bottom:-6px;margin-top:-6px}.xl\:sl--mx-1\.5{margin-left:-6px;margin-right:-6px}.xl\:sl--my-2\.5{margin-bottom:-10px;margin-top:-10px}.xl\:sl--mx-2\.5{margin-left:-10px;margin-right:-10px}.xl\:sl--my-3\.5{margin-bottom:-14px;margin-top:-14px}.xl\:sl--mx-3\.5{margin-left:-14px;margin-right:-14px}.xl\:sl--my-4\.5{margin-bottom:-18px;margin-top:-18px}.xl\:sl--mx-4\.5{margin-left:-18px;margin-right:-18px}.xl\:sl-mt-0{margin-top:0}.xl\:sl-mr-0{margin-right:0}.xl\:sl-mb-0{margin-bottom:0}.xl\:sl-ml-0{margin-left:0}.xl\:sl-mt-1{margin-top:4px}.xl\:sl-mr-1{margin-right:4px}.xl\:sl-mb-1{margin-bottom:4px}.xl\:sl-ml-1{margin-left:4px}.xl\:sl-mt-2{margin-top:8px}.xl\:sl-mr-2{margin-right:8px}.xl\:sl-mb-2{margin-bottom:8px}.xl\:sl-ml-2{margin-left:8px}.xl\:sl-mt-3{margin-top:12px}.xl\:sl-mr-3{margin-right:12px}.xl\:sl-mb-3{margin-bottom:12px}.xl\:sl-ml-3{margin-left:12px}.xl\:sl-mt-4{margin-top:16px}.xl\:sl-mr-4{margin-right:16px}.xl\:sl-mb-4{margin-bottom:16px}.xl\:sl-ml-4{margin-left:16px}.xl\:sl-mt-5{margin-top:20px}.xl\:sl-mr-5{margin-right:20px}.xl\:sl-mb-5{margin-bottom:20px}.xl\:sl-ml-5{margin-left:20px}.xl\:sl-mt-6{margin-top:24px}.xl\:sl-mr-6{margin-right:24px}.xl\:sl-mb-6{margin-bottom:24px}.xl\:sl-ml-6{margin-left:24px}.xl\:sl-mt-7{margin-top:28px}.xl\:sl-mr-7{margin-right:28px}.xl\:sl-mb-7{margin-bottom:28px}.xl\:sl-ml-7{margin-left:28px}.xl\:sl-mt-8{margin-top:32px}.xl\:sl-mr-8{margin-right:32px}.xl\:sl-mb-8{margin-bottom:32px}.xl\:sl-ml-8{margin-left:32px}.xl\:sl-mt-9{margin-top:36px}.xl\:sl-mr-9{margin-right:36px}.xl\:sl-mb-9{margin-bottom:36px}.xl\:sl-ml-9{margin-left:36px}.xl\:sl-mt-10{margin-top:40px}.xl\:sl-mr-10{margin-right:40px}.xl\:sl-mb-10{margin-bottom:40px}.xl\:sl-ml-10{margin-left:40px}.xl\:sl-mt-11{margin-top:44px}.xl\:sl-mr-11{margin-right:44px}.xl\:sl-mb-11{margin-bottom:44px}.xl\:sl-ml-11{margin-left:44px}.xl\:sl-mt-12{margin-top:48px}.xl\:sl-mr-12{margin-right:48px}.xl\:sl-mb-12{margin-bottom:48px}.xl\:sl-ml-12{margin-left:48px}.xl\:sl-mt-14{margin-top:56px}.xl\:sl-mr-14{margin-right:56px}.xl\:sl-mb-14{margin-bottom:56px}.xl\:sl-ml-14{margin-left:56px}.xl\:sl-mt-16{margin-top:64px}.xl\:sl-mr-16{margin-right:64px}.xl\:sl-mb-16{margin-bottom:64px}.xl\:sl-ml-16{margin-left:64px}.xl\:sl-mt-20{margin-top:80px}.xl\:sl-mr-20{margin-right:80px}.xl\:sl-mb-20{margin-bottom:80px}.xl\:sl-ml-20{margin-left:80px}.xl\:sl-mt-24{margin-top:96px}.xl\:sl-mr-24{margin-right:96px}.xl\:sl-mb-24{margin-bottom:96px}.xl\:sl-ml-24{margin-left:96px}.xl\:sl-mt-28{margin-top:112px}.xl\:sl-mr-28{margin-right:112px}.xl\:sl-mb-28{margin-bottom:112px}.xl\:sl-ml-28{margin-left:112px}.xl\:sl-mt-32{margin-top:128px}.xl\:sl-mr-32{margin-right:128px}.xl\:sl-mb-32{margin-bottom:128px}.xl\:sl-ml-32{margin-left:128px}.xl\:sl-mt-36{margin-top:144px}.xl\:sl-mr-36{margin-right:144px}.xl\:sl-mb-36{margin-bottom:144px}.xl\:sl-ml-36{margin-left:144px}.xl\:sl-mt-40{margin-top:160px}.xl\:sl-mr-40{margin-right:160px}.xl\:sl-mb-40{margin-bottom:160px}.xl\:sl-ml-40{margin-left:160px}.xl\:sl-mt-44{margin-top:176px}.xl\:sl-mr-44{margin-right:176px}.xl\:sl-mb-44{margin-bottom:176px}.xl\:sl-ml-44{margin-left:176px}.xl\:sl-mt-48{margin-top:192px}.xl\:sl-mr-48{margin-right:192px}.xl\:sl-mb-48{margin-bottom:192px}.xl\:sl-ml-48{margin-left:192px}.xl\:sl-mt-52{margin-top:208px}.xl\:sl-mr-52{margin-right:208px}.xl\:sl-mb-52{margin-bottom:208px}.xl\:sl-ml-52{margin-left:208px}.xl\:sl-mt-56{margin-top:224px}.xl\:sl-mr-56{margin-right:224px}.xl\:sl-mb-56{margin-bottom:224px}.xl\:sl-ml-56{margin-left:224px}.xl\:sl-mt-60{margin-top:240px}.xl\:sl-mr-60{margin-right:240px}.xl\:sl-mb-60{margin-bottom:240px}.xl\:sl-ml-60{margin-left:240px}.xl\:sl-mt-64{margin-top:256px}.xl\:sl-mr-64{margin-right:256px}.xl\:sl-mb-64{margin-bottom:256px}.xl\:sl-ml-64{margin-left:256px}.xl\:sl-mt-72{margin-top:288px}.xl\:sl-mr-72{margin-right:288px}.xl\:sl-mb-72{margin-bottom:288px}.xl\:sl-ml-72{margin-left:288px}.xl\:sl-mt-80{margin-top:320px}.xl\:sl-mr-80{margin-right:320px}.xl\:sl-mb-80{margin-bottom:320px}.xl\:sl-ml-80{margin-left:320px}.xl\:sl-mt-96{margin-top:384px}.xl\:sl-mr-96{margin-right:384px}.xl\:sl-mb-96{margin-bottom:384px}.xl\:sl-ml-96{margin-left:384px}.xl\:sl-mt-auto{margin-top:auto}.xl\:sl-mr-auto{margin-right:auto}.xl\:sl-mb-auto{margin-bottom:auto}.xl\:sl-ml-auto{margin-left:auto}.xl\:sl-mt-px{margin-top:1px}.xl\:sl-mr-px{margin-right:1px}.xl\:sl-mb-px{margin-bottom:1px}.xl\:sl-ml-px{margin-left:1px}.xl\:sl-mt-0\.5{margin-top:2px}.xl\:sl-mr-0\.5{margin-right:2px}.xl\:sl-mb-0\.5{margin-bottom:2px}.xl\:sl-ml-0\.5{margin-left:2px}.xl\:sl-mt-1\.5{margin-top:6px}.xl\:sl-mr-1\.5{margin-right:6px}.xl\:sl-mb-1\.5{margin-bottom:6px}.xl\:sl-ml-1\.5{margin-left:6px}.xl\:sl-mt-2\.5{margin-top:10px}.xl\:sl-mr-2\.5{margin-right:10px}.xl\:sl-mb-2\.5{margin-bottom:10px}.xl\:sl-ml-2\.5{margin-left:10px}.xl\:sl-mt-3\.5{margin-top:14px}.xl\:sl-mr-3\.5{margin-right:14px}.xl\:sl-mb-3\.5{margin-bottom:14px}.xl\:sl-ml-3\.5{margin-left:14px}.xl\:sl-mt-4\.5{margin-top:18px}.xl\:sl-mr-4\.5{margin-right:18px}.xl\:sl-mb-4\.5{margin-bottom:18px}.xl\:sl-ml-4\.5{margin-left:18px}.xl\:sl--mt-0{margin-top:0}.xl\:sl--mr-0{margin-right:0}.xl\:sl--mb-0{margin-bottom:0}.xl\:sl--ml-0{margin-left:0}.xl\:sl--mt-1{margin-top:-4px}.xl\:sl--mr-1{margin-right:-4px}.xl\:sl--mb-1{margin-bottom:-4px}.xl\:sl--ml-1{margin-left:-4px}.xl\:sl--mt-2{margin-top:-8px}.xl\:sl--mr-2{margin-right:-8px}.xl\:sl--mb-2{margin-bottom:-8px}.xl\:sl--ml-2{margin-left:-8px}.xl\:sl--mt-3{margin-top:-12px}.xl\:sl--mr-3{margin-right:-12px}.xl\:sl--mb-3{margin-bottom:-12px}.xl\:sl--ml-3{margin-left:-12px}.xl\:sl--mt-4{margin-top:-16px}.xl\:sl--mr-4{margin-right:-16px}.xl\:sl--mb-4{margin-bottom:-16px}.xl\:sl--ml-4{margin-left:-16px}.xl\:sl--mt-5{margin-top:-20px}.xl\:sl--mr-5{margin-right:-20px}.xl\:sl--mb-5{margin-bottom:-20px}.xl\:sl--ml-5{margin-left:-20px}.xl\:sl--mt-6{margin-top:-24px}.xl\:sl--mr-6{margin-right:-24px}.xl\:sl--mb-6{margin-bottom:-24px}.xl\:sl--ml-6{margin-left:-24px}.xl\:sl--mt-7{margin-top:-28px}.xl\:sl--mr-7{margin-right:-28px}.xl\:sl--mb-7{margin-bottom:-28px}.xl\:sl--ml-7{margin-left:-28px}.xl\:sl--mt-8{margin-top:-32px}.xl\:sl--mr-8{margin-right:-32px}.xl\:sl--mb-8{margin-bottom:-32px}.xl\:sl--ml-8{margin-left:-32px}.xl\:sl--mt-9{margin-top:-36px}.xl\:sl--mr-9{margin-right:-36px}.xl\:sl--mb-9{margin-bottom:-36px}.xl\:sl--ml-9{margin-left:-36px}.xl\:sl--mt-10{margin-top:-40px}.xl\:sl--mr-10{margin-right:-40px}.xl\:sl--mb-10{margin-bottom:-40px}.xl\:sl--ml-10{margin-left:-40px}.xl\:sl--mt-11{margin-top:-44px}.xl\:sl--mr-11{margin-right:-44px}.xl\:sl--mb-11{margin-bottom:-44px}.xl\:sl--ml-11{margin-left:-44px}.xl\:sl--mt-12{margin-top:-48px}.xl\:sl--mr-12{margin-right:-48px}.xl\:sl--mb-12{margin-bottom:-48px}.xl\:sl--ml-12{margin-left:-48px}.xl\:sl--mt-14{margin-top:-56px}.xl\:sl--mr-14{margin-right:-56px}.xl\:sl--mb-14{margin-bottom:-56px}.xl\:sl--ml-14{margin-left:-56px}.xl\:sl--mt-16{margin-top:-64px}.xl\:sl--mr-16{margin-right:-64px}.xl\:sl--mb-16{margin-bottom:-64px}.xl\:sl--ml-16{margin-left:-64px}.xl\:sl--mt-20{margin-top:-80px}.xl\:sl--mr-20{margin-right:-80px}.xl\:sl--mb-20{margin-bottom:-80px}.xl\:sl--ml-20{margin-left:-80px}.xl\:sl--mt-24{margin-top:-96px}.xl\:sl--mr-24{margin-right:-96px}.xl\:sl--mb-24{margin-bottom:-96px}.xl\:sl--ml-24{margin-left:-96px}.xl\:sl--mt-28{margin-top:-112px}.xl\:sl--mr-28{margin-right:-112px}.xl\:sl--mb-28{margin-bottom:-112px}.xl\:sl--ml-28{margin-left:-112px}.xl\:sl--mt-32{margin-top:-128px}.xl\:sl--mr-32{margin-right:-128px}.xl\:sl--mb-32{margin-bottom:-128px}.xl\:sl--ml-32{margin-left:-128px}.xl\:sl--mt-36{margin-top:-144px}.xl\:sl--mr-36{margin-right:-144px}.xl\:sl--mb-36{margin-bottom:-144px}.xl\:sl--ml-36{margin-left:-144px}.xl\:sl--mt-40{margin-top:-160px}.xl\:sl--mr-40{margin-right:-160px}.xl\:sl--mb-40{margin-bottom:-160px}.xl\:sl--ml-40{margin-left:-160px}.xl\:sl--mt-44{margin-top:-176px}.xl\:sl--mr-44{margin-right:-176px}.xl\:sl--mb-44{margin-bottom:-176px}.xl\:sl--ml-44{margin-left:-176px}.xl\:sl--mt-48{margin-top:-192px}.xl\:sl--mr-48{margin-right:-192px}.xl\:sl--mb-48{margin-bottom:-192px}.xl\:sl--ml-48{margin-left:-192px}.xl\:sl--mt-52{margin-top:-208px}.xl\:sl--mr-52{margin-right:-208px}.xl\:sl--mb-52{margin-bottom:-208px}.xl\:sl--ml-52{margin-left:-208px}.xl\:sl--mt-56{margin-top:-224px}.xl\:sl--mr-56{margin-right:-224px}.xl\:sl--mb-56{margin-bottom:-224px}.xl\:sl--ml-56{margin-left:-224px}.xl\:sl--mt-60{margin-top:-240px}.xl\:sl--mr-60{margin-right:-240px}.xl\:sl--mb-60{margin-bottom:-240px}.xl\:sl--ml-60{margin-left:-240px}.xl\:sl--mt-64{margin-top:-256px}.xl\:sl--mr-64{margin-right:-256px}.xl\:sl--mb-64{margin-bottom:-256px}.xl\:sl--ml-64{margin-left:-256px}.xl\:sl--mt-72{margin-top:-288px}.xl\:sl--mr-72{margin-right:-288px}.xl\:sl--mb-72{margin-bottom:-288px}.xl\:sl--ml-72{margin-left:-288px}.xl\:sl--mt-80{margin-top:-320px}.xl\:sl--mr-80{margin-right:-320px}.xl\:sl--mb-80{margin-bottom:-320px}.xl\:sl--ml-80{margin-left:-320px}.xl\:sl--mt-96{margin-top:-384px}.xl\:sl--mr-96{margin-right:-384px}.xl\:sl--mb-96{margin-bottom:-384px}.xl\:sl--ml-96{margin-left:-384px}.xl\:sl--mt-px{margin-top:-1px}.xl\:sl--mr-px{margin-right:-1px}.xl\:sl--mb-px{margin-bottom:-1px}.xl\:sl--ml-px{margin-left:-1px}.xl\:sl--mt-0\.5{margin-top:-2px}.xl\:sl--mr-0\.5{margin-right:-2px}.xl\:sl--mb-0\.5{margin-bottom:-2px}.xl\:sl--ml-0\.5{margin-left:-2px}.xl\:sl--mt-1\.5{margin-top:-6px}.xl\:sl--mr-1\.5{margin-right:-6px}.xl\:sl--mb-1\.5{margin-bottom:-6px}.xl\:sl--ml-1\.5{margin-left:-6px}.xl\:sl--mt-2\.5{margin-top:-10px}.xl\:sl--mr-2\.5{margin-right:-10px}.xl\:sl--mb-2\.5{margin-bottom:-10px}.xl\:sl--ml-2\.5{margin-left:-10px}.xl\:sl--mt-3\.5{margin-top:-14px}.xl\:sl--mr-3\.5{margin-right:-14px}.xl\:sl--mb-3\.5{margin-bottom:-14px}.xl\:sl--ml-3\.5{margin-left:-14px}.xl\:sl--mt-4\.5{margin-top:-18px}.xl\:sl--mr-4\.5{margin-right:-18px}.xl\:sl--mb-4\.5{margin-bottom:-18px}.xl\:sl--ml-4\.5{margin-left:-18px}.xl\:sl-max-h-full{max-height:100%}.xl\:sl-max-h-screen{max-height:100vh}.xl\:sl-max-w-none{max-width:none}.xl\:sl-max-w-full{max-width:100%}.xl\:sl-max-w-min{max-width:-moz-min-content;max-width:min-content}.xl\:sl-max-w-max{max-width:-moz-max-content;max-width:max-content}.xl\:sl-max-w-prose{max-width:65ch}.xl\:sl-min-h-full{min-height:100%}.xl\:sl-min-h-screen{min-height:100vh}.xl\:sl-min-w-full{min-width:100%}.xl\:sl-min-w-min{min-width:-moz-min-content;min-width:min-content}.xl\:sl-min-w-max{min-width:-moz-max-content;min-width:max-content}.xl\:sl-p-0{padding:0}.xl\:sl-p-1{padding:4px}.xl\:sl-p-2{padding:8px}.xl\:sl-p-3{padding:12px}.xl\:sl-p-4{padding:16px}.xl\:sl-p-5{padding:20px}.xl\:sl-p-6{padding:24px}.xl\:sl-p-7{padding:28px}.xl\:sl-p-8{padding:32px}.xl\:sl-p-9{padding:36px}.xl\:sl-p-10{padding:40px}.xl\:sl-p-11{padding:44px}.xl\:sl-p-12{padding:48px}.xl\:sl-p-14{padding:56px}.xl\:sl-p-16{padding:64px}.xl\:sl-p-20{padding:80px}.xl\:sl-p-24{padding:96px}.xl\:sl-p-28{padding:112px}.xl\:sl-p-32{padding:128px}.xl\:sl-p-36{padding:144px}.xl\:sl-p-40{padding:160px}.xl\:sl-p-44{padding:176px}.xl\:sl-p-48{padding:192px}.xl\:sl-p-52{padding:208px}.xl\:sl-p-56{padding:224px}.xl\:sl-p-60{padding:240px}.xl\:sl-p-64{padding:256px}.xl\:sl-p-72{padding:288px}.xl\:sl-p-80{padding:320px}.xl\:sl-p-96{padding:384px}.xl\:sl-p-px{padding:1px}.xl\:sl-p-0\.5{padding:2px}.xl\:sl-p-1\.5{padding:6px}.xl\:sl-p-2\.5{padding:10px}.xl\:sl-p-3\.5{padding:14px}.xl\:sl-p-4\.5{padding:18px}.xl\:sl-py-0{padding-bottom:0;padding-top:0}.xl\:sl-px-0{padding-left:0;padding-right:0}.xl\:sl-py-1{padding-bottom:4px;padding-top:4px}.xl\:sl-px-1{padding-left:4px;padding-right:4px}.xl\:sl-py-2{padding-bottom:8px;padding-top:8px}.xl\:sl-px-2{padding-left:8px;padding-right:8px}.xl\:sl-py-3{padding-bottom:12px;padding-top:12px}.xl\:sl-px-3{padding-left:12px;padding-right:12px}.xl\:sl-py-4{padding-bottom:16px;padding-top:16px}.xl\:sl-px-4{padding-left:16px;padding-right:16px}.xl\:sl-py-5{padding-bottom:20px;padding-top:20px}.xl\:sl-px-5{padding-left:20px;padding-right:20px}.xl\:sl-py-6{padding-bottom:24px;padding-top:24px}.xl\:sl-px-6{padding-left:24px;padding-right:24px}.xl\:sl-py-7{padding-bottom:28px;padding-top:28px}.xl\:sl-px-7{padding-left:28px;padding-right:28px}.xl\:sl-py-8{padding-bottom:32px;padding-top:32px}.xl\:sl-px-8{padding-left:32px;padding-right:32px}.xl\:sl-py-9{padding-bottom:36px;padding-top:36px}.xl\:sl-px-9{padding-left:36px;padding-right:36px}.xl\:sl-py-10{padding-bottom:40px;padding-top:40px}.xl\:sl-px-10{padding-left:40px;padding-right:40px}.xl\:sl-py-11{padding-bottom:44px;padding-top:44px}.xl\:sl-px-11{padding-left:44px;padding-right:44px}.xl\:sl-py-12{padding-bottom:48px;padding-top:48px}.xl\:sl-px-12{padding-left:48px;padding-right:48px}.xl\:sl-py-14{padding-bottom:56px;padding-top:56px}.xl\:sl-px-14{padding-left:56px;padding-right:56px}.xl\:sl-py-16{padding-bottom:64px;padding-top:64px}.xl\:sl-px-16{padding-left:64px;padding-right:64px}.xl\:sl-py-20{padding-bottom:80px;padding-top:80px}.xl\:sl-px-20{padding-left:80px;padding-right:80px}.xl\:sl-py-24{padding-bottom:96px;padding-top:96px}.xl\:sl-px-24{padding-left:96px;padding-right:96px}.xl\:sl-py-28{padding-bottom:112px;padding-top:112px}.xl\:sl-px-28{padding-left:112px;padding-right:112px}.xl\:sl-py-32{padding-bottom:128px;padding-top:128px}.xl\:sl-px-32{padding-left:128px;padding-right:128px}.xl\:sl-py-36{padding-bottom:144px;padding-top:144px}.xl\:sl-px-36{padding-left:144px;padding-right:144px}.xl\:sl-py-40{padding-bottom:160px;padding-top:160px}.xl\:sl-px-40{padding-left:160px;padding-right:160px}.xl\:sl-py-44{padding-bottom:176px;padding-top:176px}.xl\:sl-px-44{padding-left:176px;padding-right:176px}.xl\:sl-py-48{padding-bottom:192px;padding-top:192px}.xl\:sl-px-48{padding-left:192px;padding-right:192px}.xl\:sl-py-52{padding-bottom:208px;padding-top:208px}.xl\:sl-px-52{padding-left:208px;padding-right:208px}.xl\:sl-py-56{padding-bottom:224px;padding-top:224px}.xl\:sl-px-56{padding-left:224px;padding-right:224px}.xl\:sl-py-60{padding-bottom:240px;padding-top:240px}.xl\:sl-px-60{padding-left:240px;padding-right:240px}.xl\:sl-py-64{padding-bottom:256px;padding-top:256px}.xl\:sl-px-64{padding-left:256px;padding-right:256px}.xl\:sl-py-72{padding-bottom:288px;padding-top:288px}.xl\:sl-px-72{padding-left:288px;padding-right:288px}.xl\:sl-py-80{padding-bottom:320px;padding-top:320px}.xl\:sl-px-80{padding-left:320px;padding-right:320px}.xl\:sl-py-96{padding-bottom:384px;padding-top:384px}.xl\:sl-px-96{padding-left:384px;padding-right:384px}.xl\:sl-py-px{padding-bottom:1px;padding-top:1px}.xl\:sl-px-px{padding-left:1px;padding-right:1px}.xl\:sl-py-0\.5{padding-bottom:2px;padding-top:2px}.xl\:sl-px-0\.5{padding-left:2px;padding-right:2px}.xl\:sl-py-1\.5{padding-bottom:6px;padding-top:6px}.xl\:sl-px-1\.5{padding-left:6px;padding-right:6px}.xl\:sl-py-2\.5{padding-bottom:10px;padding-top:10px}.xl\:sl-px-2\.5{padding-left:10px;padding-right:10px}.xl\:sl-py-3\.5{padding-bottom:14px;padding-top:14px}.xl\:sl-px-3\.5{padding-left:14px;padding-right:14px}.xl\:sl-py-4\.5{padding-bottom:18px;padding-top:18px}.xl\:sl-px-4\.5{padding-left:18px;padding-right:18px}.xl\:sl-pt-0{padding-top:0}.xl\:sl-pr-0{padding-right:0}.xl\:sl-pb-0{padding-bottom:0}.xl\:sl-pl-0{padding-left:0}.xl\:sl-pt-1{padding-top:4px}.xl\:sl-pr-1{padding-right:4px}.xl\:sl-pb-1{padding-bottom:4px}.xl\:sl-pl-1{padding-left:4px}.xl\:sl-pt-2{padding-top:8px}.xl\:sl-pr-2{padding-right:8px}.xl\:sl-pb-2{padding-bottom:8px}.xl\:sl-pl-2{padding-left:8px}.xl\:sl-pt-3{padding-top:12px}.xl\:sl-pr-3{padding-right:12px}.xl\:sl-pb-3{padding-bottom:12px}.xl\:sl-pl-3{padding-left:12px}.xl\:sl-pt-4{padding-top:16px}.xl\:sl-pr-4{padding-right:16px}.xl\:sl-pb-4{padding-bottom:16px}.xl\:sl-pl-4{padding-left:16px}.xl\:sl-pt-5{padding-top:20px}.xl\:sl-pr-5{padding-right:20px}.xl\:sl-pb-5{padding-bottom:20px}.xl\:sl-pl-5{padding-left:20px}.xl\:sl-pt-6{padding-top:24px}.xl\:sl-pr-6{padding-right:24px}.xl\:sl-pb-6{padding-bottom:24px}.xl\:sl-pl-6{padding-left:24px}.xl\:sl-pt-7{padding-top:28px}.xl\:sl-pr-7{padding-right:28px}.xl\:sl-pb-7{padding-bottom:28px}.xl\:sl-pl-7{padding-left:28px}.xl\:sl-pt-8{padding-top:32px}.xl\:sl-pr-8{padding-right:32px}.xl\:sl-pb-8{padding-bottom:32px}.xl\:sl-pl-8{padding-left:32px}.xl\:sl-pt-9{padding-top:36px}.xl\:sl-pr-9{padding-right:36px}.xl\:sl-pb-9{padding-bottom:36px}.xl\:sl-pl-9{padding-left:36px}.xl\:sl-pt-10{padding-top:40px}.xl\:sl-pr-10{padding-right:40px}.xl\:sl-pb-10{padding-bottom:40px}.xl\:sl-pl-10{padding-left:40px}.xl\:sl-pt-11{padding-top:44px}.xl\:sl-pr-11{padding-right:44px}.xl\:sl-pb-11{padding-bottom:44px}.xl\:sl-pl-11{padding-left:44px}.xl\:sl-pt-12{padding-top:48px}.xl\:sl-pr-12{padding-right:48px}.xl\:sl-pb-12{padding-bottom:48px}.xl\:sl-pl-12{padding-left:48px}.xl\:sl-pt-14{padding-top:56px}.xl\:sl-pr-14{padding-right:56px}.xl\:sl-pb-14{padding-bottom:56px}.xl\:sl-pl-14{padding-left:56px}.xl\:sl-pt-16{padding-top:64px}.xl\:sl-pr-16{padding-right:64px}.xl\:sl-pb-16{padding-bottom:64px}.xl\:sl-pl-16{padding-left:64px}.xl\:sl-pt-20{padding-top:80px}.xl\:sl-pr-20{padding-right:80px}.xl\:sl-pb-20{padding-bottom:80px}.xl\:sl-pl-20{padding-left:80px}.xl\:sl-pt-24{padding-top:96px}.xl\:sl-pr-24{padding-right:96px}.xl\:sl-pb-24{padding-bottom:96px}.xl\:sl-pl-24{padding-left:96px}.xl\:sl-pt-28{padding-top:112px}.xl\:sl-pr-28{padding-right:112px}.xl\:sl-pb-28{padding-bottom:112px}.xl\:sl-pl-28{padding-left:112px}.xl\:sl-pt-32{padding-top:128px}.xl\:sl-pr-32{padding-right:128px}.xl\:sl-pb-32{padding-bottom:128px}.xl\:sl-pl-32{padding-left:128px}.xl\:sl-pt-36{padding-top:144px}.xl\:sl-pr-36{padding-right:144px}.xl\:sl-pb-36{padding-bottom:144px}.xl\:sl-pl-36{padding-left:144px}.xl\:sl-pt-40{padding-top:160px}.xl\:sl-pr-40{padding-right:160px}.xl\:sl-pb-40{padding-bottom:160px}.xl\:sl-pl-40{padding-left:160px}.xl\:sl-pt-44{padding-top:176px}.xl\:sl-pr-44{padding-right:176px}.xl\:sl-pb-44{padding-bottom:176px}.xl\:sl-pl-44{padding-left:176px}.xl\:sl-pt-48{padding-top:192px}.xl\:sl-pr-48{padding-right:192px}.xl\:sl-pb-48{padding-bottom:192px}.xl\:sl-pl-48{padding-left:192px}.xl\:sl-pt-52{padding-top:208px}.xl\:sl-pr-52{padding-right:208px}.xl\:sl-pb-52{padding-bottom:208px}.xl\:sl-pl-52{padding-left:208px}.xl\:sl-pt-56{padding-top:224px}.xl\:sl-pr-56{padding-right:224px}.xl\:sl-pb-56{padding-bottom:224px}.xl\:sl-pl-56{padding-left:224px}.xl\:sl-pt-60{padding-top:240px}.xl\:sl-pr-60{padding-right:240px}.xl\:sl-pb-60{padding-bottom:240px}.xl\:sl-pl-60{padding-left:240px}.xl\:sl-pt-64{padding-top:256px}.xl\:sl-pr-64{padding-right:256px}.xl\:sl-pb-64{padding-bottom:256px}.xl\:sl-pl-64{padding-left:256px}.xl\:sl-pt-72{padding-top:288px}.xl\:sl-pr-72{padding-right:288px}.xl\:sl-pb-72{padding-bottom:288px}.xl\:sl-pl-72{padding-left:288px}.xl\:sl-pt-80{padding-top:320px}.xl\:sl-pr-80{padding-right:320px}.xl\:sl-pb-80{padding-bottom:320px}.xl\:sl-pl-80{padding-left:320px}.xl\:sl-pt-96{padding-top:384px}.xl\:sl-pr-96{padding-right:384px}.xl\:sl-pb-96{padding-bottom:384px}.xl\:sl-pl-96{padding-left:384px}.xl\:sl-pt-px{padding-top:1px}.xl\:sl-pr-px{padding-right:1px}.xl\:sl-pb-px{padding-bottom:1px}.xl\:sl-pl-px{padding-left:1px}.xl\:sl-pt-0\.5{padding-top:2px}.xl\:sl-pr-0\.5{padding-right:2px}.xl\:sl-pb-0\.5{padding-bottom:2px}.xl\:sl-pl-0\.5{padding-left:2px}.xl\:sl-pt-1\.5{padding-top:6px}.xl\:sl-pr-1\.5{padding-right:6px}.xl\:sl-pb-1\.5{padding-bottom:6px}.xl\:sl-pl-1\.5{padding-left:6px}.xl\:sl-pt-2\.5{padding-top:10px}.xl\:sl-pr-2\.5{padding-right:10px}.xl\:sl-pb-2\.5{padding-bottom:10px}.xl\:sl-pl-2\.5{padding-left:10px}.xl\:sl-pt-3\.5{padding-top:14px}.xl\:sl-pr-3\.5{padding-right:14px}.xl\:sl-pb-3\.5{padding-bottom:14px}.xl\:sl-pl-3\.5{padding-left:14px}.xl\:sl-pt-4\.5{padding-top:18px}.xl\:sl-pr-4\.5{padding-right:18px}.xl\:sl-pb-4\.5{padding-bottom:18px}.xl\:sl-pl-4\.5{padding-left:18px}.xl\:sl-static{position:static}.xl\:sl-fixed{position:fixed}.xl\:sl-absolute{position:absolute}.xl\:sl-relative{position:relative}.xl\:sl-sticky{position:-webkit-sticky;position:sticky}.xl\:sl-visible{visibility:visible}.xl\:sl-invisible{visibility:hidden}.sl-group:hover .xl\:group-hover\:sl-visible{visibility:visible}.sl-group:hover .xl\:group-hover\:sl-invisible{visibility:hidden}.sl-group:focus .xl\:group-focus\:sl-visible{visibility:visible}.sl-group:focus .xl\:group-focus\:sl-invisible{visibility:hidden}.xl\:sl-w-0{width:0}.xl\:sl-w-1{width:4px}.xl\:sl-w-2{width:8px}.xl\:sl-w-3{width:12px}.xl\:sl-w-4{width:16px}.xl\:sl-w-5{width:20px}.xl\:sl-w-6{width:24px}.xl\:sl-w-7{width:28px}.xl\:sl-w-8{width:32px}.xl\:sl-w-9{width:36px}.xl\:sl-w-10{width:40px}.xl\:sl-w-11{width:44px}.xl\:sl-w-12{width:48px}.xl\:sl-w-14{width:56px}.xl\:sl-w-16{width:64px}.xl\:sl-w-20{width:80px}.xl\:sl-w-24{width:96px}.xl\:sl-w-28{width:112px}.xl\:sl-w-32{width:128px}.xl\:sl-w-36{width:144px}.xl\:sl-w-40{width:160px}.xl\:sl-w-44{width:176px}.xl\:sl-w-48{width:192px}.xl\:sl-w-52{width:208px}.xl\:sl-w-56{width:224px}.xl\:sl-w-60{width:240px}.xl\:sl-w-64{width:256px}.xl\:sl-w-72{width:288px}.xl\:sl-w-80{width:320px}.xl\:sl-w-96{width:384px}.xl\:sl-w-auto{width:auto}.xl\:sl-w-px{width:1px}.xl\:sl-w-0\.5{width:2px}.xl\:sl-w-1\.5{width:6px}.xl\:sl-w-2\.5{width:10px}.xl\:sl-w-3\.5{width:14px}.xl\:sl-w-4\.5{width:18px}.xl\:sl-w-xs{width:20px}.xl\:sl-w-sm{width:24px}.xl\:sl-w-md{width:32px}.xl\:sl-w-lg{width:36px}.xl\:sl-w-xl{width:44px}.xl\:sl-w-2xl{width:52px}.xl\:sl-w-3xl{width:60px}.xl\:sl-w-1\/2{width:50%}.xl\:sl-w-1\/3{width:33.333333%}.xl\:sl-w-2\/3{width:66.666667%}.xl\:sl-w-1\/4{width:25%}.xl\:sl-w-2\/4{width:50%}.xl\:sl-w-3\/4{width:75%}.xl\:sl-w-1\/5{width:20%}.xl\:sl-w-2\/5{width:40%}.xl\:sl-w-3\/5{width:60%}.xl\:sl-w-4\/5{width:80%}.xl\:sl-w-1\/6{width:16.666667%}.xl\:sl-w-2\/6{width:33.333333%}.xl\:sl-w-3\/6{width:50%}.xl\:sl-w-4\/6{width:66.666667%}.xl\:sl-w-5\/6{width:83.333333%}.xl\:sl-w-full{width:100%}.xl\:sl-w-screen{width:100vw}.xl\:sl-w-min{width:-moz-min-content;width:min-content}.xl\:sl-w-max{width:-moz-max-content;width:max-content}}:root,[data-theme=light],[data-theme=light] .sl-inverted .sl-inverted,[data-theme=light] .sl-inverted .sl-inverted .sl-inverted .sl-inverted{--text-h:0;--text-s:0%;--text-l:15%;--shadow-sm:0px 0px 1px rgba(67,90,111,.3);--shadow-md:0px 2px 4px -2px rgba(0,0,0,.25),0px 0px 1px rgba(67,90,111,.3);--shadow-lg:0 4px 17px rgba(67,90,111,.2),0 2px 3px rgba(0,0,0,.1),inset 0 0 0 .5px var(--color-canvas-pure),0 0 0 .5px rgba(0,0,0,.2);--shadow-xl:0px 0px 1px rgba(67,90,111,.3),0px 8px 10px -4px rgba(67,90,111,.45);--shadow-2xl:0px 0px 1px rgba(67,90,111,.3),0px 16px 24px -8px rgba(67,90,111,.45);--drop-shadow-default1:0 0 0.5px rgba(0,0,0,.6);--drop-shadow-default2:0 2px 5px rgba(67,90,111,.3);--color-text-heading:hsla(var(--text-h),var(--text-s),max(3,calc(var(--text-l) - 15)),1);--color-text:hsla(var(--text-h),var(--text-s),var(--text-l),1);--color-text-paragraph:hsla(var(--text-h),var(--text-s),var(--text-l),0.9);--color-text-muted:hsla(var(--text-h),var(--text-s),var(--text-l),0.7);--color-text-light:hsla(var(--text-h),var(--text-s),var(--text-l),0.55);--color-text-disabled:hsla(var(--text-h),var(--text-s),var(--text-l),0.3);--canvas-h:218;--canvas-s:40%;--canvas-l:100%;--color-canvas:hsla(var(--canvas-h),var(--canvas-s),var(--canvas-l),1);--color-canvas-dark:#2d3748;--color-canvas-pure:#fff;--color-canvas-tint:rgba(245,247,250,.5);--color-canvas-50:#f5f7fa;--color-canvas-100:#ebeef5;--color-canvas-200:#e0e6f0;--color-canvas-300:#d5ddeb;--color-canvas-400:#cbd5e7;--color-canvas-500:#c0cde2;--color-canvas-dialog:#fff;--color-border-dark:hsla(var(--canvas-h),30%,72%,0.5);--color-border:hsla(var(--canvas-h),32%,78%,0.5);--color-border-light:hsla(var(--canvas-h),24%,84%,0.5);--color-border-input:hsla(var(--canvas-h),24%,72%,0.8);--color-border-button:hsla(var(--canvas-h),24%,20%,0.65);--primary-h:202;--primary-s:100%;--primary-l:55%;--color-text-primary:#0081cc;--color-primary-dark:#1891d8;--color-primary-darker:#126fa5;--color-primary:#19abff;--color-primary-light:#52bfff;--color-primary-tint:rgba(77,190,255,.25);--color-on-primary:#fff;--success-h:156;--success-s:95%;--success-l:37%;--color-text-success:#05c779;--color-success-dark:#138b5b;--color-success-darker:#0f6c47;--color-success:#05b870;--color-success-light:#06db86;--color-success-tint:rgba(81,251,183,.25);--color-on-success:#fff;--warning-h:20;--warning-s:90%;--warning-l:56%;--color-text-warning:#c2470a;--color-warning-dark:#d35d22;--color-warning-darker:#9e461a;--color-warning:#f46d2a;--color-warning-light:#f7925f;--color-warning-tint:rgba(246,139,85,.25);--color-on-warning:#fff;--danger-h:0;--danger-s:84%;--danger-l:63%;--color-text-danger:#bc1010;--color-danger-dark:#d83b3b;--color-danger-darker:#af2323;--color-danger:#f05151;--color-danger-light:#f58e8e;--color-danger-tint:rgba(241,91,91,.25);--color-on-danger:#fff;color:var(--color-text)}:root .sl-inverted,[data-theme=light] .sl-inverted,[data-theme=light] .sl-inverted .sl-inverted .sl-inverted{--text-h:0;--text-s:0%;--text-l:86%;--shadow-sm:0px 0px 1px rgba(11,13,19,.5);--shadow-md:0px 2px 4px -2px rgba(0,0,0,.35),0px 0px 1px rgba(11,13,19,.4);--shadow-lg:0 2px 14px rgba(0,0,0,.55),0 0 0 0.5px hsla(0,0%,100%,.2);--shadow-xl:0px 0px 1px rgba(11,13,19,.4),0px 8px 10px -4px rgba(11,13,19,.55);--shadow-2xl:0px 0px 1px rgba(11,13,19,.4),0px 16px 24px -8px rgba(11,13,19,.55);--drop-shadow-default1:0 0 0.5px hsla(0,0%,100%,.5);--drop-shadow-default2:0 3px 8px rgba(0,0,0,.6);--color-text-heading:hsla(var(--text-h),var(--text-s),max(3,calc(var(--text-l) - 15)),1);--color-text:hsla(var(--text-h),var(--text-s),var(--text-l),1);--color-text-paragraph:hsla(var(--text-h),var(--text-s),var(--text-l),0.9);--color-text-muted:hsla(var(--text-h),var(--text-s),var(--text-l),0.7);--color-text-light:hsla(var(--text-h),var(--text-s),var(--text-l),0.55);--color-text-disabled:hsla(var(--text-h),var(--text-s),var(--text-l),0.3);--canvas-h:218;--canvas-s:32%;--canvas-l:10%;--color-canvas:hsla(var(--canvas-h),var(--canvas-s),var(--canvas-l),1);--color-canvas-dark:#2d3748;--color-canvas-pure:#0c1018;--color-canvas-tint:rgba(60,76,103,.2);--color-canvas-50:#3c4c67;--color-canvas-100:#2d394e;--color-canvas-200:#212a3b;--color-canvas-300:#19212e;--color-canvas-400:#171e2b;--color-canvas-500:#151c28;--color-canvas-dialog:#2d394e;--color-border-dark:hsla(var(--canvas-h),24%,23%,0.5);--color-border:hsla(var(--canvas-h),26%,28%,0.5);--color-border-light:hsla(var(--canvas-h),19%,34%,0.5);--color-border-input:hsla(var(--canvas-h),19%,30%,0.8);--color-border-button:hsla(var(--canvas-h),19%,80%,0.65);--primary-h:202;--primary-s:90%;--primary-l:51%;--color-text-primary:#66c7ff;--color-primary-dark:#1f83bd;--color-primary-darker:#186491;--color-primary:#12a0f3;--color-primary-light:#42b3f5;--color-primary-tint:rgba(85,187,246,.25);--color-on-primary:#fff;--success-h:156;--success-s:95%;--success-l:67%;--color-text-success:#41f1ab;--color-success-dark:#47dca0;--color-success-darker:#24bc7f;--color-success:#62f3b9;--color-success-light:#a0f8d5;--color-success-tint:rgba(89,243,181,.25);--color-on-success:#fff;--warning-h:20;--warning-s:90%;--warning-l:50%;--color-text-warning:#ec7d46;--color-warning-dark:#b55626;--color-warning-darker:#8b421d;--color-warning:#e75d18;--color-warning-light:#ec7d46;--color-warning-tint:rgba(238,142,93,.25);--color-on-warning:#fff;--danger-h:0;--danger-s:84%;--danger-l:43%;--color-text-danger:#e74b4b;--color-danger-dark:#972626;--color-danger-darker:#721d1d;--color-danger:#c11a1a;--color-danger-light:#e22828;--color-danger-tint:rgba(234,98,98,.25);--color-on-danger:#fff;color:var(--color-text)}[data-theme=dark],[data-theme=dark] .sl-inverted .sl-inverted,[data-theme=dark] .sl-inverted .sl-inverted .sl-inverted .sl-inverted{--text-h:0;--text-s:0%;--text-l:85%;--shadow-sm:0px 0px 1px rgba(11,13,19,.5);--shadow-md:0px 2px 4px -2px rgba(0,0,0,.35),0px 0px 1px rgba(11,13,19,.4);--shadow-lg:0 2px 14px rgba(0,0,0,.55),0 0 0 0.5px hsla(0,0%,100%,.2);--shadow-xl:0px 0px 1px rgba(11,13,19,.4),0px 8px 10px -4px rgba(11,13,19,.55);--shadow-2xl:0px 0px 1px rgba(11,13,19,.4),0px 16px 24px -8px rgba(11,13,19,.55);--drop-shadow-default1:0 0 0.5px hsla(0,0%,100%,.5);--drop-shadow-default2:0 3px 8px rgba(0,0,0,.6);--color-text-heading:hsla(var(--text-h),var(--text-s),max(3,calc(var(--text-l) - 15)),1);--color-text:hsla(var(--text-h),var(--text-s),var(--text-l),1);--color-text-paragraph:hsla(var(--text-h),var(--text-s),var(--text-l),0.9);--color-text-muted:hsla(var(--text-h),var(--text-s),var(--text-l),0.7);--color-text-light:hsla(var(--text-h),var(--text-s),var(--text-l),0.55);--color-text-disabled:hsla(var(--text-h),var(--text-s),var(--text-l),0.3);--canvas-h:218;--canvas-s:32%;--canvas-l:8%;--color-canvas:hsla(var(--canvas-h),var(--canvas-s),var(--canvas-l),1);--color-canvas-dark:#2d3748;--color-canvas-pure:#090c11;--color-canvas-tint:rgba(57,71,96,.2);--color-canvas-50:#262f40;--color-canvas-100:#1a212d;--color-canvas-200:#121821;--color-canvas-300:#0e131a;--color-canvas-400:#0c1017;--color-canvas-500:#0c1017;--color-canvas-dialog:#1a212d;--color-border-dark:hsla(var(--canvas-h),24%,21%,0.5);--color-border:hsla(var(--canvas-h),26%,26%,0.5);--color-border-light:hsla(var(--canvas-h),19%,32%,0.5);--color-border-input:hsla(var(--canvas-h),19%,28%,0.8);--color-border-button:hsla(var(--canvas-h),19%,80%,0.65);--primary-h:202;--primary-s:80%;--primary-l:36%;--color-text-primary:#66c7ff;--color-primary-dark:#1c5a7d;--color-primary-darker:#154560;--color-primary:#126fa5;--color-primary-light:#1685c5;--color-primary-tint:rgba(21,130,193,.25);--color-on-primary:#fff;--success-h:156;--success-s:95%;--success-l:37%;--color-text-success:#4be7a9;--color-success-dark:#145239;--color-success-darker:#10422e;--color-success:#0f6c47;--color-success-light:#128255;--color-success-tint:rgba(26,188,123,.25);--color-on-success:#fff;--warning-h:20;--warning-s:90%;--warning-l:56%;--color-text-warning:#e28150;--color-warning-dark:#7d4021;--color-warning-darker:#61311a;--color-warning:#9e461a;--color-warning-light:#c1551f;--color-warning-tint:rgba(184,81,30,.25);--color-on-warning:#fff;--danger-h:0;--danger-s:84%;--danger-l:63%;--color-text-danger:#d55;--color-danger-dark:#892929;--color-danger-darker:#6a2020;--color-danger:#af2323;--color-danger-light:#d12929;--color-danger-tint:rgba(179,35,35,.25);--color-on-danger:#fff;color:var(--color-text)}[data-theme=dark] .sl-inverted,[data-theme=dark] .sl-inverted .sl-inverted .sl-inverted{--text-h:0;--text-s:0%;--text-l:89%;--shadow-sm:0px 0px 1px rgba(11,13,19,.5);--shadow-md:0px 2px 4px -2px rgba(0,0,0,.35),0px 0px 1px rgba(11,13,19,.4);--shadow-lg:0 2px 14px rgba(0,0,0,.55),0 0 0 0.5px hsla(0,0%,100%,.2);--shadow-xl:0px 0px 1px rgba(11,13,19,.4),0px 8px 10px -4px rgba(11,13,19,.55);--shadow-2xl:0px 0px 1px rgba(11,13,19,.4),0px 16px 24px -8px rgba(11,13,19,.55);--drop-shadow-default1:0 0 0.5px hsla(0,0%,100%,.5);--drop-shadow-default2:0 3px 8px rgba(0,0,0,.6);--color-text-heading:hsla(var(--text-h),var(--text-s),max(3,calc(var(--text-l) - 15)),1);--color-text:hsla(var(--text-h),var(--text-s),var(--text-l),1);--color-text-paragraph:hsla(var(--text-h),var(--text-s),var(--text-l),0.9);--color-text-muted:hsla(var(--text-h),var(--text-s),var(--text-l),0.7);--color-text-light:hsla(var(--text-h),var(--text-s),var(--text-l),0.55);--color-text-disabled:hsla(var(--text-h),var(--text-s),var(--text-l),0.3);--canvas-h:218;--canvas-s:32%;--canvas-l:13%;--color-canvas:hsla(var(--canvas-h),var(--canvas-s),var(--canvas-l),1);--color-canvas-dark:#2d3748;--color-canvas-pure:#111722;--color-canvas-tint:rgba(66,83,112,.2);--color-canvas-50:#2b374a;--color-canvas-100:#222b3a;--color-canvas-200:#1a212e;--color-canvas-300:#141a24;--color-canvas-400:#121721;--color-canvas-500:#121721;--color-canvas-dialog:#222b3a;--color-border-dark:hsla(var(--canvas-h),24%,26%,0.5);--color-border:hsla(var(--canvas-h),26%,31%,0.5);--color-border-light:hsla(var(--canvas-h),19%,37%,0.5);--color-border-input:hsla(var(--canvas-h),19%,33%,0.8);--color-border-button:hsla(var(--canvas-h),19%,80%,0.65);--primary-h:202;--primary-s:80%;--primary-l:33%;--color-text-primary:#66c7ff;--color-primary-dark:#1a5475;--color-primary-darker:#14425c;--color-primary:#116697;--color-primary-light:#147cb8;--color-primary-tint:rgba(21,130,193,.25);--color-on-primary:#fff;--success-h:156;--success-s:95%;--success-l:67%;--color-text-success:#4be7a9;--color-success-dark:#25986a;--color-success-darker:#1c7350;--color-success:#1bc581;--color-success-light:#28e297;--color-success-tint:rgba(26,188,123,.25);--color-on-success:#fff;--warning-h:20;--warning-s:90%;--warning-l:50%;--color-text-warning:#e28150;--color-warning-dark:#713a1e;--color-warning-darker:#552b16;--color-warning:#914018;--color-warning-light:#ab4c1c;--color-warning-tint:rgba(184,81,30,.25);--color-on-warning:#fff;--danger-h:0;--danger-s:84%;--danger-l:43%;--color-text-danger:#d55;--color-danger-dark:#5e1c1c;--color-danger-darker:#471515;--color-danger:#771818;--color-danger-light:#911d1d;--color-danger-tint:rgba(179,35,35,.25);--color-on-danger:#fff;color:var(--color-text)}.sl-elements{font-size:13px}.sl-elements .svg-inline--fa{display:inline-block}.sl-elements .DocsSkeleton{animation:skeleton-glow .5s linear infinite alternate;background:rgba(206,217,224,.2);background-clip:padding-box!important;border-color:rgba(206,217,224,.2)!important;border-radius:2px;box-shadow:none!important;color:transparent!important;cursor:default;pointer-events:none;user-select:none}.sl-elements .Model{--fs-code:12px}.sl-elements .ElementsTableOfContentsItem:hover{color:inherit;text-decoration:none}.sl-elements .ParameterGrid{align-items:center;display:grid;grid-template-columns:fit-content(120px) 20px auto;margin-bottom:16px;padding-bottom:0;row-gap:3px}.sl-elements .TryItPanel>:nth-child(2){overflow:auto}.sl-elements .OperationParametersContent{max-height:162px}.sl-elements .Checkbox{max-width:15px;padding-right:3px}.sl-elements .TextForCheckBox{padding-left:9px;padding-top:6px}.sl-elements .TextRequestBody{margin-bottom:16px;max-height:200px;overflow-y:auto;padding-bottom:0}.sl-elements .HttpOperation .JsonSchemaViewer .sl-markdown-viewer p,.sl-elements .HttpOperation__Parameters .sl-markdown-viewer p,.sl-elements .Model .JsonSchemaViewer .sl-markdown-viewer p{font-size:12px;line-height:1.5em}.sl-elements .HttpOperation div[role=tablist]{overflow-x:auto}.sl-elements .HttpService .ServerInfo .sl-panel__titlebar div{height:100%;min-height:36px} \ No newline at end of file diff --git a/core/app/c/[communitySlug]/pubs/PubList.tsx b/core/app/c/[communitySlug]/pubs/PubList.tsx index db5639a384..540f4be5b7 100644 --- a/core/app/c/[communitySlug]/pubs/PubList.tsx +++ b/core/app/c/[communitySlug]/pubs/PubList.tsx @@ -5,6 +5,7 @@ import type { AutoReturnType } from "~/lib/types" import { Suspense } from "react" import { BookOpen } from "lucide-react" +import { AutomationEvent } from "db/public" import { Empty, EmptyContent, @@ -129,7 +130,11 @@ const PaginatedPubListInner = async ( communitySlug={props.communitySlug} moveFrom={stageForPub?.moveConstraintSources} moveTo={stageForPub?.moveConstraints} - actionInstances={stageForPub?.actionInstances} + manualAutomations={stageForPub?.automations?.filter((automation) => + automation?.triggers.some( + (trigger) => trigger.event === AutomationEvent.manual + ) + )} userId={props.userId} canEditAllPubs={canEditAllPubs} canArchiveAllPubs={canArchiveAllPubs} @@ -240,7 +245,7 @@ export const PaginatedPubList: React.FC = async (props) = }), getStages( { communityId: props.communityId, userId: props.userId }, - { withActionInstances: "full" } + { withAutomations: AutomationEvent.manual } ).execute(), getPubFields({ communityId: props.communityId }).executeTakeFirstOrThrow(), ]) diff --git a/core/app/c/[communitySlug]/pubs/[pubId]/components/RelatedPubsTableWrapper.tsx b/core/app/c/[communitySlug]/pubs/[pubId]/components/RelatedPubsTableWrapper.tsx index a6c34b2426..45632b7a7c 100644 --- a/core/app/c/[communitySlug]/pubs/[pubId]/components/RelatedPubsTableWrapper.tsx +++ b/core/app/c/[communitySlug]/pubs/[pubId]/components/RelatedPubsTableWrapper.tsx @@ -3,7 +3,7 @@ import type { FullProcessedPubWithForm } from "~/lib/server" import { Info } from "ui/icon" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "ui/tooltip" -import { PubsRunActionDropDownMenu } from "~/app/components/ActionUI/PubsRunActionDropDownMenu" +import { PubsRunAutomationsDropDownMenu } from "~/app/components/AutomationUI/PubsRunAutomationDropDownMenu" import { RelatedPubsTable } from "./RelatedPubsTable" const NoActions = () => { @@ -24,9 +24,16 @@ const NoActions = () => { ) } -const getRelatedPubRunActionsDropdowns = (row: FullProcessedPubWithForm) => { - return row.stage && row.stage?.actionInstances.length > 0 ? ( - +const getRelatedPubRunActionsDropdowns = ( + row: FullProcessedPubWithForm, + userCanOverrideAutomationConditions: boolean +) => { + return row.stage && row.stage?.automations.length > 0 ? ( + ) : ( ) @@ -35,6 +42,7 @@ const getRelatedPubRunActionsDropdowns = (row: FullProcessedPubWithForm) => { type Props = { pub: FullProcessedPubWithForm userCanRunActions: boolean + userCanOverrideAutomationConditions: boolean } export const RelatedPubsTableWrapper = async (props: Props) => { @@ -46,7 +54,8 @@ export const RelatedPubsTableWrapper = async (props: Props) => { ? { ...a, [value.relatedPubId]: getRelatedPubRunActionsDropdowns( - value.relatedPub + value.relatedPub, + props.userCanOverrideAutomationConditions ), } : a, diff --git a/core/app/c/[communitySlug]/pubs/[pubId]/page.tsx b/core/app/c/[communitySlug]/pubs/[pubId]/page.tsx index 6c8b9ccebd..f79635eade 100644 --- a/core/app/c/[communitySlug]/pubs/[pubId]/page.tsx +++ b/core/app/c/[communitySlug]/pubs/[pubId]/page.tsx @@ -1,24 +1,27 @@ -import type { CommunitiesId, PubsId } from "db/public" import type { Metadata } from "next" import { cache } from "react" import Link from "next/link" import { notFound } from "next/navigation" -import { BookOpen, Eye } from "lucide-react" +import { BookOpen, Eye, Pencil } from "lucide-react" -import { Capabilities, MembershipType } from "db/public" +import { + AutomationEvent, + Capabilities, + type CommunitiesId, + MembershipType, + type PubsId, +} from "db/public" import { Button } from "ui/button" -import { Pencil } from "ui/icon" import { PubFieldProvider } from "ui/pubFields" import { StagesProvider, stagesDAO } from "ui/stages" import { Tooltip, TooltipContent, TooltipTrigger } from "ui/tooltip" import { tryCatch } from "utils/try-catch" -import Move from "~/app/c/[communitySlug]/stages/components/Move" -import { MembersList } from "~/app/components//Memberships/MembersList" -import { PubsRunActionDropDownMenu } from "~/app/components/ActionUI/PubsRunActionDropDownMenu" +import { PubsRunAutomationsDropDownMenu } from "~/app/components/AutomationUI/PubsRunAutomationDropDownMenu" import { FormSwitcher } from "~/app/components/FormSwitcher/FormSwitcher" import { AddMemberDialog } from "~/app/components/Memberships/AddMemberDialog" +import { MembersList } from "~/app/components/Memberships/MembersList" import { CreatePubButton } from "~/app/components/pubs/CreatePubButton" import { RemovePubButton } from "~/app/components/pubs/RemovePubButton" import { getPageLoginData } from "~/lib/authentication/loginData" @@ -28,7 +31,6 @@ import { userCan, userCanRunActionsAllPubs, } from "~/lib/authorization/capabilities" -import { getStageActions } from "~/lib/db/queries" import { constructRedirectToPubEditPage } from "~/lib/links" import { getPubByForm, getPubTitle } from "~/lib/pubs" import { getPubsWithRelatedValues, NotFoundError } from "~/lib/server" @@ -39,6 +41,7 @@ import { redirectToPubDetailPage, redirectToUnauthorized } from "~/lib/server/na import { getPubFields } from "~/lib/server/pubFields" import { getStages } from "~/lib/server/stages" import { ContentLayout } from "../../ContentLayout" +import Move from "../../stages/components/Move" import { addPubMember, addUserWithPubMembership, @@ -59,7 +62,10 @@ const getPubsWithRelatedValuesCached = cache(async (pubId: PubsId, communityId: withPubType: true, withRelatedPubs: true, withStage: true, - withStageActionInstances: true, + withStageAutomations: { + detail: "full", + filter: [AutomationEvent.manual], + }, withMembers: true, depth: 3, } @@ -125,8 +131,6 @@ export default async function Page(props: { // This is safe because we've already explicitly checked authorization for the root pub const pubPromise = getPubsWithRelatedValuesCached(pubId, community.id) - const actionsPromise = getStageActions({ pubId: pubId }).execute() - // if a specific form is provided, we use the slug // otherwise, we get the default form for the pub type of the current pub const getFormProps = formSlug @@ -144,7 +148,7 @@ export default async function Page(props: { canRemoveMember, canCreateRelatedPub, canRunActionsAllPubs, - actions, + canOverrideAutomationConditions, communityStages, withExtraPubValues, form, @@ -158,8 +162,12 @@ export default async function Page(props: { userCan(Capabilities.addPubMember, { type: MembershipType.pub, pubId }, user.id), userCan(Capabilities.removePubMember, { type: MembershipType.pub, pubId }, user.id), userCan(Capabilities.createRelatedPub, { type: MembershipType.pub, pubId }, user.id), + userCan( + Capabilities.overrideAutomationConditions, + { type: MembershipType.community, communityId: community.id }, + user.id + ), userCanRunActionsAllPubs(communitySlug), - actionsPromise, communityStagesPromise, userCan( Capabilities.seeExtraPubValues, @@ -303,10 +311,16 @@ export default async function Page(props: { ) : null}
Actions
- {actions && actions.length > 0 && stage && canRunActions ? ( + {pub.stage?.fullAutomations && + pub.stage?.fullAutomations.length > 0 && + stage && + canRunActions ? (
- @@ -364,6 +378,9 @@ export default async function Page(props: {
)} diff --git a/core/app/c/[communitySlug]/pubs/page.tsx b/core/app/c/[communitySlug]/pubs/page.tsx index 6bea9a6e04..ad44b07318 100644 --- a/core/app/c/[communitySlug]/pubs/page.tsx +++ b/core/app/c/[communitySlug]/pubs/page.tsx @@ -27,7 +27,6 @@ type Props = { export default async function Page(props: Props) { const searchParams = await props.searchParams - const _params = await props.params const [{ user }, community] = await Promise.all([getPageLoginData(), findCommunityBySlug()]) diff --git a/core/app/c/[communitySlug]/settings/tokens/CreateTokenForm.tsx b/core/app/c/[communitySlug]/settings/tokens/CreateTokenForm.tsx index ce10ee7262..f572040f80 100644 --- a/core/app/c/[communitySlug]/settings/tokens/CreateTokenForm.tsx +++ b/core/app/c/[communitySlug]/settings/tokens/CreateTokenForm.tsx @@ -145,7 +145,7 @@ export const CreateTokenForm = ({ onSuccess }: CreateTokenFormProps) => {
{form.formState.errors?.permissions && ( -
+

{form.formState.errors?.permissions?.root?.message}

)} @@ -154,7 +154,9 @@ export const CreateTokenForm = ({ onSuccess }: CreateTokenFormProps) => { }} /> {form.formState.errors?.root && ( -

{form.formState.errors?.root?.message}

+

+ {form.formState.errors?.root?.message} +

)} - name?: undefined - } - | { name: string; config?: undefined } -) { - const loginData = await getLoginData() - if (!loginData || !loginData.user) { - return ApiError.NOT_LOGGED_IN - } - - const authorized = await userCan( - Capabilities.manageStage, - { type: MembershipType.stage, stageId }, - loginData.user.id - ) - - if (!authorized) { - return ApiError.UNAUTHORIZED - } - - await updateActionInstance(actionInstanceId, { - config: props.config, - name: props.name, - }).executeTakeFirstOrThrow() - - return { - success: true, - report: "Action updated", - } -}) - -export const deleteAction = defineServerAction(async function deleteAction( - actionId: ActionInstancesId, - stageId: StagesId -) { - const loginData = await getLoginData() - if (!loginData || !loginData.user) { - return ApiError.NOT_LOGGED_IN - } - - const authorized = await userCan( - Capabilities.manageStage, - { type: MembershipType.stage, stageId }, - loginData.user.id - ) - - if (!authorized) { - return ApiError.UNAUTHORIZED - } - - try { - await removeActionInstance(actionId).executeTakeFirstOrThrow() - } catch (error) { - return { - error: "Failed to delete action", - cause: error, - } - } finally { - await revalidateTagsForCommunity(["action_instances"]) - } -}) +// export const addAction = defineServerAction(async function addAction( +// stageId: StagesId, +// actionName: Action +// ) { +// const loginData = await getLoginData() +// if (!loginData || !loginData.user) { +// return ApiError.NOT_LOGGED_IN +// } + +// const { user } = loginData + +// const authorized = await userCan( +// Capabilities.manageStage, +// { type: MembershipType.stage, stageId }, +// user.id +// ) + +// if (!authorized) { +// return ApiError.UNAUTHORIZED +// } +// try { +// await createActionInstance({ +// action: actionName, +// }).executeTakeFirstOrThrow() +// } catch (error) { +// return { +// error: "Failed to add action", +// cause: error, +// } +// } +// }) + +// export const updateAction = defineServerAction(async function updateAction( +// actionInstanceId: ActionInstancesId, +// stageId: StagesId, +// props: +// | { +// config: Record +// name?: undefined +// } +// | { name: string; config?: undefined } +// ) { +// const loginData = await getLoginData() +// if (!loginData || !loginData.user) { +// return ApiError.NOT_LOGGED_IN +// } + +// const authorized = await userCan( +// Capabilities.manageStage, +// { type: MembershipType.stage, stageId }, +// loginData.user.id +// ) + +// if (!authorized) { +// return ApiError.UNAUTHORIZED +// } + +// await updateActionInstance(actionInstanceId, { +// config: props.config, +// }).executeTakeFirstOrThrow() + +// return { +// success: true, +// report: "Action updated", +// } +// }) + +// export const deleteAction = defineServerAction(async function deleteAction( +// actionId: ActionInstancesId, +// stageId: StagesId +// ) { +// const loginData = await getLoginData() +// if (!loginData || !loginData.user) { +// return ApiError.NOT_LOGGED_IN +// } + +// const authorized = await userCan( +// Capabilities.manageStage, +// { type: MembershipType.stage, stageId }, +// loginData.user.id +// ) + +// if (!authorized) { +// return ApiError.UNAUTHORIZED +// } + +// try { +// await removeActionInstance(actionId).executeTakeFirstOrThrow() +// } catch (error) { +// return { +// error: "Failed to delete action", +// cause: error, +// } +// } finally { +// await revalidateTagsForCommunity(["action_instances"]) +// } +// }) export const addOrUpdateAutomation = defineServerAction(async function addOrUpdateAutomation({ stageId, @@ -383,7 +372,10 @@ export const addOrUpdateAutomation = defineServerAction(async function addOrUpda automationId?: AutomationsId data: CreateAutomationsSchema }) { - const loginData = await getLoginData() + const [loginData, community] = await Promise.all([getLoginData(), findCommunityBySlug()]) + if (!community) { + return ApiError.COMMUNITY_NOT_FOUND + } if (!loginData || !loginData.user) { return ApiError.NOT_LOGGED_IN } @@ -399,19 +391,27 @@ export const addOrUpdateAutomation = defineServerAction(async function addOrUpda } try { - await createOrUpdateAutomationWithCycleCheck({ - automationId, - actionInstanceId: data.actionInstanceId as ActionInstancesId, - event: data.event, - config: { - actionConfig: data.actionConfig ?? null, - automationConfig: - "automationConfig" in data && data.automationConfig - ? data.automationConfig - : null, - }, - sourceActionInstanceId: - "sourceActionInstanceId" in data ? data.sourceActionInstanceId : undefined, + await upsertAutomationWithCycleCheck({ + id: automationId, + name: data.name, + description: data.description ?? null, + icon: data.icon ?? null, + communityId: community.id, + stageId, + conditionEvaluationTiming: data.conditionEvaluationTiming ?? null, + + triggers: data.triggers.map((trigger) => ({ + event: trigger.event, + config: trigger.config ?? null, + sourceAutomationId: trigger.sourceAutomationId ?? null, + })), + actionInstances: [ + { + ...data.action, + id: data.action.actionInstanceId, + }, + ], + condition: data.condition, }) } catch (error) { logger.error(error) @@ -427,7 +427,6 @@ export const addOrUpdateAutomation = defineServerAction(async function addOrUpda error: automationId ? "Failed to update automation" : "Failed to create automation", cause: error, } - } finally { } }) @@ -451,52 +450,14 @@ export const deleteAutomation = defineServerAction(async function deleteAutomati } try { - const deletedAutomation = await autoRevalidate( + const _deletedAutomation = await autoRevalidate( removeAutomation(automationId).qb.returningAll() ).executeTakeFirstOrThrow() - if (!deletedAutomation) { - return { - error: "Failed to delete automation", - cause: `Automation with id ${automationId} not found`, - } - } - - if (deletedAutomation.event !== Event.pubInStageForDuration) { - return - } - - const actionInstance = await getActionInstance( - deletedAutomation.actionInstanceId - ).executeTakeFirst() - - if (!actionInstance) { - // something is wrong here - captureException( - new Error( - `Action instance not found for automation ${automationId} while trying to unschedule jobs` - ) - ) - return - } - - const pubsInStage = await getPubIdsInStage(actionInstance.stageId).executeTakeFirst() - if (!pubsInStage) { - // we don't need to unschedule any jobs, as there are no pubs this automation could have been applied to - return + return { + success: true, + report: "Automation deleted", } - - logger.debug(`Unscheduling jobs for automation ${automationId}`) - await Promise.all( - pubsInStage.pubIds.map(async (pubInStageId) => - unscheduleAction({ - actionInstanceId: actionInstance.id, - pubId: pubInStageId, - stageId: actionInstance.stageId, - event: Event.pubInStageForDuration, - }) - ) - ) } catch (error) { logger.error(error) return { @@ -509,6 +470,36 @@ export const deleteAutomation = defineServerAction(async function deleteAutomati } }) +export const duplicateAutomation = defineServerAction(async function duplicateAutomation( + automationId: AutomationsId, + stageId: StagesId +) { + const loginData = await getLoginData() + if (!loginData || !loginData.user) { + return ApiError.NOT_LOGGED_IN + } + + const authorized = await userCan( + Capabilities.manageStage, + { type: MembershipType.stage, stageId }, + loginData.user.id + ) + + if (!authorized) { + return ApiError.UNAUTHORIZED + } + + try { + const _duplicatedAutomation = await duplicateAutomationDb(automationId) + } catch (error) { + logger.error(error) + return { + error: "Failed to duplicate automation", + cause: error, + } + } +}) + export const removeStageMember = defineServerAction(async function removeStageMember( userId: UsersId, stageId: StagesId diff --git a/core/app/c/[communitySlug]/stages/manage/components/editor/StageEditorContext.tsx b/core/app/c/[communitySlug]/stages/manage/components/editor/StageEditorContext.tsx index 9aa3afb6e5..1443fc309a 100644 --- a/core/app/c/[communitySlug]/stages/manage/components/editor/StageEditorContext.tsx +++ b/core/app/c/[communitySlug]/stages/manage/components/editor/StageEditorContext.tsx @@ -103,7 +103,7 @@ export const StageEditorProvider = (props: StageEditorProps) => { setHasSelection(false) }, [deleteStagesAndMoveConstraints]) - const _editStage = useCallback( + const editStage = useCallback( (stage?: CommunityStage) => { router.push( stage @@ -123,7 +123,7 @@ export const StageEditorProvider = (props: StageEditorProps) => { hasSelection, getNodePosition, setNodePositions, - editStage: _editStage, + editStage, } satisfies StageEditorContext return {props.children} diff --git a/core/app/c/[communitySlug]/stages/manage/components/editor/StageEditorNode.tsx b/core/app/c/[communitySlug]/stages/manage/components/editor/StageEditorNode.tsx index 026643d68b..ef711dfdc3 100644 --- a/core/app/c/[communitySlug]/stages/manage/components/editor/StageEditorNode.tsx +++ b/core/app/c/[communitySlug]/stages/manage/components/editor/StageEditorNode.tsx @@ -8,7 +8,7 @@ import Link from "next/link" import { Handle, Position } from "reactflow" import { Button } from "ui/button" -import { Settings } from "ui/icon" +import { BookOpen, Bot, Settings, Users } from "ui/icon" import { cn } from "utils" import { useCommunity } from "~/app/components/providers/CommunityProvider" @@ -107,7 +107,7 @@ export const StageEditorNode = memo((props: NodeProps<{ stage: CommunityStage }>
  • diff --git a/core/app/c/[communitySlug]/stages/manage/components/editor/StagePanelCard.tsx b/core/app/c/[communitySlug]/stages/manage/components/editor/StagePanelCard.tsx new file mode 100644 index 0000000000..cf84c8cd33 --- /dev/null +++ b/core/app/c/[communitySlug]/stages/manage/components/editor/StagePanelCard.tsx @@ -0,0 +1,9 @@ +import { CardHeader } from "ui/card" + +export const StagePanelCardHeader = (props: React.ComponentProps) => { + return ( + + {props.children} + + ) +} diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/StageNameInput.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/StageNameInput.tsx index 07e6555b80..ca12a59732 100644 --- a/core/app/c/[communitySlug]/stages/manage/components/panel/StageNameInput.tsx +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/StageNameInput.tsx @@ -25,7 +25,7 @@ export const StageNameInput = (props: Props) => { return (
    - -

    {action.description}

    -
    {props.children}
    -
    - - ) -} diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelActions.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelActions.tsx deleted file mode 100644 index fd2b54cc30..0000000000 --- a/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelActions.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import type { StagesId, UsersId } from "db/public" - -import { Suspense } from "react" - -import { Card, CardContent } from "ui/card" - -import { ActionConfigForm } from "~/app/components/ActionUI/ActionConfigForm" -import { SkeletonCard } from "~/app/components/skeletons/SkeletonCard" -import { getLoginData } from "~/lib/authentication/loginData" -import { getStage, getStageActions } from "~/lib/db/queries" -import { addAction, deleteAction } from "../../../actions" -import { StagePanelActionCreator } from "./StagePanelActionCreator" -import { StagePanelActionEditor } from "./StagePanelActionEditor" - -type PropsInner = { - stageId: StagesId - userId: UsersId -} - -const StagePanelActionsInner = async (props: PropsInner) => { - const [stage, actionInstances] = await Promise.all([ - getStage(props.stageId, props.userId).executeTakeFirst(), - getStageActions({ stageId: props.stageId }).execute(), - ]) - - if (stage === undefined) { - return - } - - const onAddAction = addAction.bind(null, stage.id) - const onDeleteAction = deleteAction - - const { user } = await getLoginData() - - return ( - - -

    Actions

    - {actionInstances.length > 0 ? ( -

    - {stage.name} has {actionInstances.length} action - {actionInstances.length > 1 ? "s" : ""}. -

    - ) : ( -

    - {stage.name} has no actions. Use the button below to add one. -

    - )} -
    - {actionInstances.map((actionInstance) => ( - - - - ))} -
    - -
    -
    - ) -} - -type Props = { - stageId?: StagesId - userId: UsersId -} - -export const StagePanelActions = async (props: Props) => { - if (props.stageId === undefined) { - return - } - - return ( - }> - - - ) -} diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelAutomation.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelAutomation.tsx deleted file mode 100644 index 251a502791..0000000000 --- a/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelAutomation.tsx +++ /dev/null @@ -1,108 +0,0 @@ -"use client" - -import type { - Action, - ActionInstances, - ActionInstancesId, - AutomationsId, - CommunitiesId, - Event, - StagesId, -} from "db/public" -import type { AutomationForEvent } from "~/actions/_lib/automations" -import type { AutomationConfig } from "~/actions/types" - -import { useCallback } from "react" -import { parseAsString, useQueryState } from "nuqs" - -import { Button } from "ui/button" -import { Pencil } from "ui/icon" -import { cn } from "utils" - -import { getActionByName, getAutomationByName, humanReadableEventHydrated } from "~/actions/api" -import { useCommunity } from "~/app/components/providers/CommunityProvider" - -type Props = { - stageId: StagesId - communityId: CommunitiesId - automation: { - id: AutomationsId - event: Event - actionInstance: ActionInstances - sourceActionInstance?: ActionInstances | null - config: AutomationConfig> | null - createdAt: Date - updatedAt: Date - actionInstanceId: ActionInstancesId - sourceActionInstanceId: ActionInstancesId | null - } -} - -const ActionIcon = (props: { actionName: Action; className?: string }) => { - const action = getActionByName(props.actionName) - return -} - -export const StagePanelAutomation = (props: Props) => { - const { automation } = props - - const [, setEditingAutomationId] = useQueryState( - "automation-id", - parseAsString.withDefault("new-automation") - ) - - const onEditClick = useCallback(() => { - setEditingAutomationId(automation.id) - }, [automation.id, setEditingAutomationId]) - const community = useCommunity() - const automationSettings = getAutomationByName(automation.event) - - return ( -
    -
    -
    - - When{" "} - - { - - } - {automation.sourceActionInstance ? ( - <> - - {humanReadableEventHydrated(automation.event, community, { - automation: automation, - config: automation.config?.automationConfig ?? undefined, - sourceAction: automation.sourceActionInstance, - })} - - ) : ( - humanReadableEventHydrated(automation.event, community, { - automation: automation, - config: automation.config?.automationConfig ?? undefined, - sourceAction: automation.sourceActionInstance, - }) - )} - -
    run{" "} - - - {automation.actionInstance.name} - {" "} -
    -
    -
    - -
    -
    -
    - ) -} diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelAutomationForm.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelAutomationForm.tsx deleted file mode 100644 index c86f0355b1..0000000000 --- a/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelAutomationForm.tsx +++ /dev/null @@ -1,603 +0,0 @@ -"use client" - -import type { ActionInstances, AutomationsId, CommunitiesId, StagesId } from "db/public" -import type { ControllerRenderProps, FieldValues, UseFormReturn } from "react-hook-form" -import type { Automation, AutomationConfig, AutomationForEvent } from "~/actions/_lib/automations" -import type { getStageActions } from "~/lib/db/queries" -import type { AutoReturnType } from "~/lib/types" - -import { useCallback, useEffect, useId, useMemo, useState } from "react" -import { zodResolver } from "@hookform/resolvers/zod" -import { Trash } from "lucide-react" -import { useQueryState } from "nuqs" -import { useForm } from "react-hook-form" -import { z } from "zod" - -import { actionInstancesIdSchema, Event } from "db/public" -import { logger } from "logger" -import { Button } from "ui/button" -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "ui/dialog" -import { Form, FormField, FormItem, FormLabel, FormMessage } from "ui/form" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "ui/select" -import { FormSubmitButton } from "ui/submit-button" -import { cn } from "utils" - -import { ActionFormContext } from "~/actions/_lib/ActionForm" -import { actions, automations, getAutomationByName, humanReadableEventBase } from "~/actions/api" -import { getActionFormComponent } from "~/actions/forms" -import { useCommunity } from "~/app/components/providers/CommunityProvider" -import { isClientException, useServerAction } from "~/lib/serverActions" -import { addOrUpdateAutomation, deleteAutomation } from "../../../actions" - -type Props = { - stageId: StagesId - actionInstances: AutoReturnType["execute"] - communityId: CommunitiesId - automations: { - id: AutomationsId - event: Event - actionInstance: ActionInstances - sourceAction?: ActionInstances - - config?: AutomationConfig> | null - }[] -} - -const ActionSelector = ({ - fieldProps, - actionInstances, - label, - placeholder, - disabledActionId, - dataTestIdPrefix, -}: { - fieldProps: Omit< - ControllerRenderProps, - "name" - > - actionInstances: AutoReturnType["execute"] - label: string - placeholder: string - disabledActionId?: string - dataTestIdPrefix?: string -}) => { - return ( - - {label} - - - - ) -} - -const baseSchema = z.discriminatedUnion("event", [ - z.object({ - event: z.literal(Event.pubEnteredStage), - actionInstanceId: actionInstancesIdSchema, - }), - z.object({ - event: z.literal(Event.pubLeftStage), - actionInstanceId: actionInstancesIdSchema, - }), - z.object({ - event: z.literal(Event.actionSucceeded), - actionInstanceId: actionInstancesIdSchema, - sourceActionInstanceId: actionInstancesIdSchema, - }), - z.object({ - event: z.literal(Event.actionFailed), - actionInstanceId: actionInstancesIdSchema, - sourceActionInstanceId: actionInstancesIdSchema, - }), - z.object({ - event: z.literal(Event.webhook), - actionInstanceId: actionInstancesIdSchema, - actionConfig: z.object({}), - }), - ...Object.values(automations) - .filter( - ( - automation - ): automation is Exclude< - Automation, - { - event: - | Event.pubEnteredStage - | Event.pubLeftStage - | Event.actionSucceeded - | Event.actionFailed - | Event.webhook - } - > => - ![ - Event.pubEnteredStage, - Event.pubLeftStage, - Event.actionSucceeded, - Event.actionFailed, - Event.webhook, - ].includes(automation.event) - ) - .map((automation) => - z.object({ - event: z.literal(automation.event), - actionInstanceId: actionInstancesIdSchema, - automationConfig: automation.additionalConfig - ? automation.additionalConfig - : z.null().optional(), - }) - ), -]) - -const refineSchema = (schema: T) => { - return schema.superRefine((data, ctx) => { - if (data.event !== Event.actionSucceeded && data.event !== Event.actionFailed) { - return - } - - if (data.sourceActionInstanceId === data.actionInstanceId) { - ctx.addIssue({ - path: ["sourceActionInstanceId"], - code: z.ZodIssueCode.custom, - message: "Automations may not trigger actions in a loop", - }) - } - }) -} - -export type CreateAutomationsSchema = z.infer & { - actionConfig: Record | null -} - -export const StagePanelAutomationForm = (props: Props) => { - const [currentlyEditingAutomationId, setCurrentlyEditingAutomationId] = - useQueryState("automation-id") - const runUpsertAutomation = useServerAction(addOrUpdateAutomation) - const [isOpen, setIsOpen] = useState(false) - - const [selectedActionInstance, setSelectedActionInstance] = useState< - (typeof props.actionInstances)[number] | null - >(null) - - const actionInstance = useMemo(() => { - if (!selectedActionInstance) { - return null - } - const actionInstance = props.actionInstances.find( - (action) => action.id === selectedActionInstance.id - ) - - if (!actionInstance) { - return null - } - - return { - ...actionInstance, - action: actions[actionInstance.action], - } - }, [selectedActionInstance, props.actionInstances]) - - const actionSchema = useMemo(() => { - if (!selectedActionInstance) { - return z.object({}) - } - - if (!actionInstance) { - return z.object({}) - } - - const actionSchema = actionInstance.action.config.schema - - const schemaWithPartialDefaults = (actionSchema as z.ZodObject).partial( - (actionInstance.defaultedActionConfigKeys ?? []).reduce( - (acc, key) => { - acc[key] = true - return acc - }, - {} as Record - ) - ) - - return schemaWithPartialDefaults - }, [selectedActionInstance, actionInstance]) - - const schema = useMemo(() => { - if (!selectedActionInstance) { - return refineSchema(baseSchema) - } - const actionInstance = props.actionInstances.find( - (action) => action.id === selectedActionInstance.id - ) - if (!actionInstance) { - logger.error({ msg: "Action not found", selectedActionInstance }) - return refineSchema(baseSchema) - } - - const schemaWithAction = baseSchema.and( - z.object({ - actionConfig: actionSchema, - }) - ) - - return refineSchema(schemaWithAction) - }, [selectedActionInstance, props.actionInstances, actionSchema]) - - const form = useForm({ - resolver: zodResolver(schema), - defaultValues: { - actionInstanceId: undefined, - event: undefined, - actionConfig: null, - }, - }) - - const onSubmit = useCallback( - async (data: CreateAutomationsSchema) => { - const result = await runUpsertAutomation({ - stageId: props.stageId, - data, - automationId: currentlyEditingAutomationId as AutomationsId | undefined, - }) - if (!isClientException(result)) { - setIsOpen(false) - setCurrentlyEditingAutomationId(null) - setSelectedActionInstance(null) - form.reset() - return - } - - form.setError("root", { message: result.error }) - }, - [ - props.stageId, - runUpsertAutomation, - currentlyEditingAutomationId, - form.reset, - form.setError, - setCurrentlyEditingAutomationId, - ] - ) - - const community = useCommunity() - - const event = form.watch("event") - const selectedActionInstanceId = form.watch("actionInstanceId") - - const sourceActionInstanceId = form.watch("sourceActionInstanceId") - - // for action chaining events, filter out self-references - const isActionChainingEvent = event === Event.actionSucceeded || event === Event.actionFailed - - const { allowedEvents } = useMemo(() => { - if (!selectedActionInstanceId && !event) - return { disallowedEvents: [], allowedEvents: Object.values(Event) } - - const disallowedEvents = props.automations - .filter((automation) => { - // for regular events, disallow if same action+event already exists - if ( - automation.event !== Event.actionSucceeded && - automation.event !== Event.actionFailed - ) { - return automation.actionInstance.id === selectedActionInstanceId - } - - // for action chaining events, allow multiple automations with different watched actions - return ( - automation.actionInstance.id === selectedActionInstanceId && - automation.event === event && - automation.sourceAction?.id === sourceActionInstanceId - ) - }) - .map((automation) => automation.event) - - const allowedEvents = Object.values(Event).filter( - (event) => !disallowedEvents.includes(event) - ) - - return { disallowedEvents, allowedEvents } - }, [selectedActionInstanceId, event, props.automations, sourceActionInstanceId]) - - useEffect(() => { - const actionInstance = - props.actionInstances.find((action) => action.id === selectedActionInstanceId) ?? null - setSelectedActionInstance(actionInstance) - - if (actionInstance?.config) { - form.reset({ - ...form.getValues(), - actionConfig: actionInstance.config, - }) - } - }, [form, props.actionInstances, selectedActionInstanceId]) - - useEffect(() => { - const currentAutomation = props.automations.find( - (automation) => automation.id === currentlyEditingAutomationId - ) - - if (!currentAutomation) { - return - } - - setIsOpen(true) - const actionInstance = - props.actionInstances.find( - (action) => action.id === currentAutomation.actionInstance.id - ) ?? null - setSelectedActionInstance(actionInstance) - - form.reset({ - actionInstanceId: currentAutomation.actionInstance.id, - event: currentAutomation.event, - actionConfig: currentAutomation.config?.actionConfig, - sourceActionInstanceId: currentAutomation.sourceAction?.id, - automationConfig: currentAutomation.config?.automationConfig, - } as CreateAutomationsSchema) - }, [currentlyEditingAutomationId, props.actionInstances, props.automations, form.reset]) - - const onOpenChange = useCallback( - (open: boolean) => { - if (!open) { - form.reset() - setSelectedActionInstance(null) - setCurrentlyEditingAutomationId(null) - } - setIsOpen(open) - }, - [form.reset, setCurrentlyEditingAutomationId] - ) - - const automation = getAutomationByName(event) - - const runDeleteAutomation = useServerAction(deleteAutomation) - const onDeleteClick = useCallback(async () => { - if (!currentlyEditingAutomationId) { - return - } - - runDeleteAutomation(currentlyEditingAutomationId as AutomationsId, props.stageId) - }, [currentlyEditingAutomationId, props.stageId, runDeleteAutomation]) - - const formId = useId() - - const ActionFormComponent = useMemo(() => { - if (!selectedActionInstance) { - return null - } - - return getActionFormComponent(selectedActionInstance.action) - }, [selectedActionInstance]) - - const isExistingAutomation = !!currentlyEditingAutomationId - - return ( -
    - - - - - - - - {isExistingAutomation ? "Edit automation" : "Add automation"} - - - Set up an automation to run whenever a certain event is triggered. - - -
    - - ( - - When... - - {allowedEvents.length > 0 ? ( - <> - - - - ) : ( -

    - All events for this action have already been added. -

    - )} -
    - )} - /> - {/* Additional selector for watched action when using action chaining events */} - {isActionChainingEvent && ( - ( - - )} - /> - )} - - ( - - )} - /> - - {selectedActionInstance && event === Event.webhook && ( -
    -

    - With the following config: -

    -
    - {ActionFormComponent && ( - as UseFormReturn, - defaultFields: - selectedActionInstance.defaultedActionConfigKeys ?? - [], - context: { type: "automation" }, - }} - > - - - )} -
    -
    - )} - - {form.formState.errors.root && ( -

    - {form.formState.errors.root.message} -

    - )} - - - {currentlyEditingAutomationId && ( - - )} - - - -
    -
    -
    - ) -} diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelAutomations.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelAutomations.tsx deleted file mode 100644 index 96c667f498..0000000000 --- a/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelAutomations.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import type { CommunitiesId, StagesId, UsersId } from "db/public" - -import { Suspense } from "react" - -import { Card, CardContent } from "ui/card" - -import { SkeletonCard } from "~/app/components/skeletons/SkeletonCard" -import { getStage, getStageActions, getStageAutomations } from "~/lib/db/queries" -import { StagePanelAutomation } from "./StagePanelAutomation" -import { StagePanelAutomationForm } from "./StagePanelAutomationForm" - -type PropsInner = { - stageId: StagesId - userId: UsersId -} - -const StagePanelAutomationsInner = async (props: PropsInner) => { - const [stage, actionInstances, automations] = await Promise.all([ - getStage(props.stageId, props.userId).executeTakeFirst(), - getStageActions({ stageId: props.stageId }).execute(), - getStageAutomations(props.stageId).execute(), - ]) - - if (!stage) { - return - } - - return ( - - -

    Automations

    - {actionInstances.length > 0 ? ( - <> -
    - {automations.length > 0 ? ( - automations.map((automation) => ( - - )) - ) : ( -
    - There are no automations for {stage.name} -
    - )} -
    - - - ) : ( -
    - There are no actions for {stage.name}. Once you add an action to - this stage you can add automations to it here. -
    - )} -
    -
    - ) -} - -type Props = { - stageId?: StagesId - userId: UsersId -} - -export const StagePanelAutomations = async (props: Props) => { - if (props.stageId === undefined) { - return - } - - return ( - }> - - - ) -} diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/AddAutomationButton.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/AddAutomationButton.tsx new file mode 100644 index 0000000000..b3267fbb74 --- /dev/null +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/AddAutomationButton.tsx @@ -0,0 +1,26 @@ +"use client" + +import { parseAsString, useQueryState } from "nuqs" + +import { Button } from "ui/button" +import { Plus } from "ui/icon" + +export function AddAutomationButton() { + const [, setAutomationId] = useQueryState("automation-id", parseAsString) + + const handleClick = () => { + setAutomationId("new") + } + + return ( + + ) +} diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/ConditionBlock.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/ConditionBlock.tsx new file mode 100644 index 0000000000..a3376889c6 --- /dev/null +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/ConditionBlock.tsx @@ -0,0 +1,417 @@ +"use client" + +import type { DragEndEvent } from "@dnd-kit/core" +import type { Control, ControllerFieldState, FieldErrors } from "react-hook-form" +import type { CreateAutomationsSchema } from "./StagePanelAutomationForm" + +import { memo, useCallback, useId } from "react" +import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core" +import { restrictToParentElement, restrictToVerticalAxis } from "@dnd-kit/modifiers" +import { + SortableContext, + sortableKeyboardCoordinates, + useSortable, + verticalListSortingStrategy, +} from "@dnd-kit/sortable" +import { CSS } from "@dnd-kit/utilities" +import { Controller, useFieldArray, useWatch } from "react-hook-form" + +import { AutomationConditionBlockType, AutomationConditionType } from "db/public" +import { Button } from "ui/button" +import { GripVertical, Plus, X } from "ui/icon" +import { Input } from "ui/input" +import { Item, ItemActions, ItemContent, ItemHeader, ItemMedia } from "ui/item" +import { Label } from "ui/label" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "ui/select" +import { cn } from "utils" + +import { findRanksBetween, getRankAndIndexChanges } from "~/lib/rank" + +export type ConditionBlockFormValue = { + id?: string + kind: "block" + type: AutomationConditionBlockType + rank: string + items: ConditionItemFormValue[] +} + +export type ConditionFormValue = { + id?: string + kind: "condition" + type: AutomationConditionType + expression: string + rank: string +} + +export type ConditionItemFormValue = ConditionFormValue | ConditionBlockFormValue + +type ConditionItemProps = { + id: string + expression: string + onRemove: () => void + slug: string + fieldState: ControllerFieldState + control: Control, any> +} + +const ConditionItem = memo( + ({ id, expression, onRemove, slug, fieldState, control }: ConditionItemProps) => { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = + useSortable({ + id, + }) + const { invalid, error } = fieldState + + const style = { + transform: CSS.Translate.toString(transform), + transition, + } + + return ( + + + + + +
    + + {invalid && error && ( +

    + {error.type === "too_small" + ? "Condition cannot be empty" + : error.message} +

    + )} +
    +
    + + + +
    + ) + }, + (prevProps, nextProps) => { + return ( + prevProps.id === nextProps.id && + prevProps.expression === nextProps.expression && + prevProps.slug === nextProps.slug && + prevProps.fieldState.invalid === nextProps.fieldState.invalid && + prevProps.fieldState.error?.message === nextProps.fieldState.error?.message + ) + } +) + +type ConditionBlockProps = { + id: string + slug: string + depth?: number + onRemove?: () => void + fieldState: ControllerFieldState + control: Control +} + +export const ConditionBlock = memo( + ({ slug, depth = 0, onRemove, id, fieldState, control: controlProp }: ConditionBlockProps) => { + // this just makes the types easier + const control = controlProp as unknown as Control< + Record, + any + > + const blockType = useWatch({ control, name: `${slug}.type` }) + + const { invalid, error } = fieldState + // we don't want to higlight the block if some subitems have errors, too much info + const rootItemError = + invalid && error && "items" in error && !Array.isArray(error.items) + ? (error.items as FieldErrors)?.root + : null + + const { fields, append, move, update, remove } = useFieldArray({ + control, + name: `${slug}.items`, + }) + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ) + + const itemId = useId() + + const handleDragEnd = useCallback( + (event: DragEndEvent) => { + const changes = getRankAndIndexChanges(event, fields) + if (changes) { + move(changes.activeIndex, changes.overIndex) + const { id: _id, ...movedField } = fields[changes.activeIndex] + update(changes.overIndex, { + ...movedField, + rank: changes.rank, + }) + } + }, + [move, update, fields] + ) + + const handleAdd = useCallback( + (kind: "condition" | "block") => { + const ranks = findRanksBetween({ + start: fields[fields.length - 1]?.rank, + numberOfRanks: 1, + }) + if (kind === "condition") { + append({ + kind: "condition", + type: AutomationConditionType.jsonata, + expression: "", + rank: ranks[0], + }) + return + } + append({ + kind: "block", + type: AutomationConditionBlockType.AND, + rank: ranks[0], + items: [], + }) + }, + [append, fields] + ) + + const handleRemove = useCallback( + (index: number) => { + remove(index) + }, + [remove] + ) + + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = + useSortable({ + id, + }) + + const isNot = blockType === AutomationConditionBlockType.NOT + const maxDepth = 3 + const canNest = depth < maxDepth + + const style = { + transform: CSS.Translate.toString(transform), + transition, + } + + return ( + = 3 && "border-neutral-100 bg-white", + depth > 0 && "p-2", + rootItemError && "border-red-300" + )} + > + +
    + {depth > 0 && ( + + )} + {depth === 0 && ( + + )} + ( + + )} + /> +
    + {depth > 0 && onRemove && ( + + )} +
    + +
    + + + {fields.map((arrayField, index) => ( + + field.value?.kind === "condition" ? ( + handleRemove(index)} + slug={`${slug}.items.${index}.expression`} + /> + ) : ( + handleRemove(index)} + slug={`${slug}.items.${index}`} + /> + ) + } + /> + ))} + + +
    + + {canNest && ( + + )} +
    + + {isNot && + (fields.filter((field) => field.kind === "condition").length >= 1 || + fields.filter((field) => field.kind === "block").length >= 1) && ( +

    + NOT blocks can only contain one condition or one block +

    + )} +
    + {rootItemError && ( +

    + {rootItemError.type === "too_small" + ? "Block cannot be empty" + : rootItemError.message} +

    + )} +
    +
    + ) + }, + (prevProps, nextProps) => { + return ( + prevProps.id === nextProps.id && + prevProps.slug === nextProps.slug && + prevProps.depth === nextProps.depth && + prevProps.fieldState.invalid === nextProps.fieldState.invalid && + prevProps.fieldState.error === nextProps.fieldState.error + ) + } +) diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/IconPicker.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/IconPicker.tsx new file mode 100644 index 0000000000..246b0a6f7f --- /dev/null +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/IconPicker.tsx @@ -0,0 +1,127 @@ +import type { IconConfig } from "ui/dynamic-icon" + +import { Suspense, use, useMemo } from "react" +import { XIcon } from "lucide-react" + +import { Button } from "ui/button" +import { DynamicIcon } from "ui/dynamic-icon" +import { FieldLabel } from "ui/field" +import { Popover, PopoverContent, PopoverTrigger } from "ui/popover" +import { cn } from "utils" + +import { ColorPickerPopover } from "~/app/components/forms/elements/ColorPickerElement" +import { entries } from "~/lib/mapping" + +const DEFAULT_ICON_COLOR_PRESETS = [ + { label: "Emerald", value: "#10b981" }, + { label: "Blue", value: "#3b82f6" }, + { label: "Violet", value: "#c4b5fd" }, + { label: "Rose", value: "#f472b6" }, + { label: "Amber", value: "#f59e0b" }, + { label: "Sky", value: "#60a5fa" }, + { label: "Pink", value: "#f9a8d4" }, + { label: "Teal", value: "#2dd4bf" }, +] + +export const IconPicker = ({ + value, + onChange, +}: { + value?: IconConfig + onChange: (icon: IconConfig) => void +}) => { + return ( + + + + + + Loading...}> + + + + + ) +} + +const IconMap = import("ui/dynamic-icon").then((mod) => mod.ICON_MAP) + +export const IconPickerContent = ({ + value, + onChange, +}: { + value?: IconConfig + onChange: (icon: IconConfig) => void +}) => { + const iconMap = use(IconMap) + + const icons = useMemo( + () => ( + <> + {entries(iconMap).map(([name, Icon]) => ( + + ))} + + ), + [iconMap, onChange, value?.color, value?.name, value?.variant] + ) + + if (!iconMap) return "No icons" + + return ( +
    + Icon +
    + { + onChange({ + name: value?.name || "bot", + color, + variant: "outline", + }) + }} + presets={DEFAULT_ICON_COLOR_PRESETS} + /> + {value?.color || (value?.name && value?.name !== "bot") ? ( + + ) : null} +
    +
    {icons}
    +
    + ) +} diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelActionCreator.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/StagePanelActionCreator.tsx similarity index 54% rename from core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelActionCreator.tsx rename to core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/StagePanelActionCreator.tsx index 395d4d1a6d..51653122c3 100644 --- a/core/app/c/[communitySlug]/stages/manage/components/panel/actionsTab/StagePanelActionCreator.tsx +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/StagePanelActionCreator.tsx @@ -17,7 +17,6 @@ import { import { Tooltip, TooltipContent, TooltipTrigger } from "ui/tooltip" import { actions } from "~/actions/api" -import { useServerAction } from "~/lib/serverActions" type ActionCellProps = { action: Action @@ -31,56 +30,60 @@ const ActionCell = (props: ActionCellProps) => { return ( ) } type Props = { - onAdd: (actionName: Action["name"]) => Promise + onAdd: (actionName: Action["name"]) => unknown + children: React.ReactNode isSuperAdmin?: boolean | null } export const StagePanelActionCreator = (props: Props) => { - const runOnAdd = useServerAction(props.onAdd) const [isOpen, setIsOpen] = useState(false) const onActionSelect = useCallback( async (action: Action) => { setIsOpen(false) - runOnAdd(action.name) + props.onAdd(action.name) }, - [runOnAdd] + [props.onAdd] ) const onOpenChange = useCallback((open: boolean) => { setIsOpen(open) @@ -89,9 +92,7 @@ export const StagePanelActionCreator = (props: Props) => { return (
    - - - + {props.children} Add an action @@ -99,7 +100,7 @@ export const StagePanelActionCreator = (props: Props) => { Pick an action to add from the list below. -
    +
    {Object.values(actions) .filter((action) => !action.superAdminOnly || props.isSuperAdmin) .map((action) => ( diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/StagePanelAutomation.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/StagePanelAutomation.tsx new file mode 100644 index 0000000000..05438a4ee2 --- /dev/null +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/StagePanelAutomation.tsx @@ -0,0 +1,237 @@ +"use client" + +import type { ActionRuns, AutomationRuns, CommunitiesId, StagesId } from "db/public" +import type { FullAutomation } from "db/types" + +import { useCallback } from "react" +import { Bolt, Copy, Pencil, Trash2 } from "lucide-react" + +import { DynamicIcon, type IconConfig } from "ui/dynamic-icon" +import { Item, ItemActions, ItemContent, ItemMedia, ItemTitle } from "ui/item" +import { toast } from "ui/use-toast" + +import { getTriggerByName } from "~/actions/_lib/triggers" +import { EllipsisMenu, EllipsisMenuButton } from "~/app/components/EllipsisMenu" +import { didSucceed, useServerAction } from "~/lib/serverActions" +import { deleteAutomation, duplicateAutomation } from "../../../actions" + +type Props = { + stageId: StagesId + communityId: CommunitiesId + automation: FullAutomation & { + lastAutomationRun: (AutomationRuns & { actionRuns: ActionRuns[] }) | null + } +} + +import { useEffect, useState } from "react" +import Link from "next/link" +import { isAfter, parseISO } from "date-fns" + +import { HoverCard, HoverCardContent, HoverCardTrigger } from "ui/hover-card" +import { cn } from "utils" + +import { AutomationRunResult } from "~/app/components/AutomationUI/AutomationRunResult" +import { useCommunity } from "~/app/components/providers/CommunityProvider" +import { constructAutomationRunPage } from "~/lib/links" +import { useAutomationId } from "./useAutomationId" + +export const UpdateCircle = ( + props: AutomationRuns & { + actionRuns: ActionRuns[] + status: "success" | "failure" | "scheduled" | "partial" + stale: boolean + setStale: (stale: boolean) => void + setInitTime: (initTime: Date) => void + } +) => { + const status = props.status + + return ( + + { + if (props.stale === true) { + props.setStale(false) + props.setInitTime(new Date()) + } + }} + > +
    + {props.stale && ( + + )} +
    +
    + +
    +
    +
    + {status} +
    + +
    + {props.createdAt && ( +
    + Timestamp: + + {props.createdAt.toLocaleString()} + +
    + )} + + {props.actionRuns.at(-1)?.pubId && ( +
    + ID: + + {props.actionRuns.at(-1)?.pubId} + +
    + )} +
    + + {props.actionRuns.length > 0 && ( +
    +
    Result:
    +
    + +
    +
    + )} +
    + + + ) +} + +export const StagePanelAutomation = (props: Props) => { + const [initTime, setInitTime] = useState(new Date()) + const [isStale, setIsStale] = useState(false) + const community = useCommunity() + useEffect(() => { + if (!props.automation.lastAutomationRun) return + + // parse both as UTC to ensure proper comparison regardless of local timezone + const lastRunTime = parseISO(`${props.automation.lastAutomationRun.createdAt.toString()}Z`) + + // compare the dates using date-fns isAfter helper + if (isAfter(lastRunTime, initTime)) { + setIsStale(true) + } + }, [props.automation.lastAutomationRun]) + + const { automation } = props + + const { setAutomationId: setEditingAutomationId } = useAutomationId() + + const onEditClick = useCallback(() => { + setEditingAutomationId(automation.id) + }, [automation.id, setEditingAutomationId]) + + const runDeleteAutomation = useServerAction(deleteAutomation) + const onDeleteClick = useCallback(async () => { + const res = await runDeleteAutomation(automation.id, props.stageId) + if (didSucceed(res)) { + toast({ + title: "Automation deleted successfully", + }) + } + }, [props.stageId, runDeleteAutomation, automation.id]) + + const runDuplicateAutomation = useServerAction(duplicateAutomation) + const onDuplicateClick = useCallback(async () => { + const res = await runDuplicateAutomation(automation.id, props.stageId) + if (didSucceed(res)) { + toast({ + title: "Automation duplicated successfully", + }) + } + }, [props.stageId, runDuplicateAutomation, automation.id]) + + const triggerIcons = automation.triggers.map((trigger) => getTriggerByName(trigger.event)) + + return ( + + + + + + {automation.name} +
    + {triggerIcons.map((icon) => ( + + ))} +
    +
    + + + {props.automation.lastAutomationRun && ( + + )} + + + Edit + + + Duplicate + + + + + View run log + + + + Delete + + + +
    + ) +} diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/StagePanelAutomationEdit.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/StagePanelAutomationEdit.tsx new file mode 100644 index 0000000000..eb73091762 --- /dev/null +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/StagePanelAutomationEdit.tsx @@ -0,0 +1,66 @@ +"use client" + +import type { CommunitiesId, StagesId } from "db/public" +import type { FullAutomation } from "db/types" +import type { ActionConfigDefaultFields } from "~/lib/server/actions" + +import { ArrowLeft } from "lucide-react" + +import { Button } from "ui/button" + +import { StagePanelAutomationForm } from "./StagePanelAutomationForm" +import { useAutomationId } from "./useAutomationId" + +type Props = { + stageId: StagesId + communityId: CommunitiesId + automations: FullAutomation[] + actionConfigDefaults: ActionConfigDefaultFields +} + +export function StagePanelAutomationEdit(props: Props) { + const { automationId, setAutomationId } = useAutomationId() + + const handleBack = () => { + setAutomationId(null) + } + const isExistingAutomation = !!automationId + + return ( +
    +
    +
    + +
    +

    + {isExistingAutomation ? "Edit automation" : "Add automation"} +

    +

    + Set up an automation to run whenever a certain event is triggered. +

    +
    +
    +
    + +
    + +
    +
    + ) +} diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/StagePanelAutomationForm.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/StagePanelAutomationForm.tsx new file mode 100644 index 0000000000..6a66041c0d --- /dev/null +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/StagePanelAutomationForm.tsx @@ -0,0 +1,1096 @@ +"use client" + +import type { FullAutomation } from "db/types" +import type { + ControllerFieldState, + ControllerRenderProps, + FieldValues, + UseFormReturn, +} from "react-hook-form" +import type { IconConfig } from "ui/dynamic-icon" +import type { ZodTypeDef } from "zod" +import type { ActionConfigDefaultFields } from "~/lib/server/actions" +import type { ConditionBlockFormValue } from "./ConditionBlock" + +import { memo, useCallback, useEffect, useId, useMemo, useRef, useState } from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { Bolt, ChevronRight, Clock, X } from "lucide-react" +import { Controller, useFieldArray, useForm, useWatch } from "react-hook-form" +import { z } from "zod" + +import { + type Action, + type ActionInstancesId, + AutomationConditionBlockType, + AutomationConditionType, + AutomationEvent, + type AutomationsId, + type AutomationTriggersId, + actionInstancesIdSchema, + automationsIdSchema, + automationTriggersIdSchema, + type Communities, + type CommunitiesId, + type ConditionEvaluationTiming, + conditionEvaluationTimingSchema, + type StagesId, +} from "db/public" +import { Button } from "ui/button" +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "ui/collapsible" +import { Field, FieldDescription, FieldError, FieldLabel } from "ui/field" +import { Plus } from "ui/icon" +import { InfoButton } from "ui/info-button" +import { Input } from "ui/input" +import { Item, ItemContent, ItemHeader } from "ui/item" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "ui/select" +import { FormSubmitButton } from "ui/submit-button" +import { type TokenContext, TokenProvider } from "ui/tokens" +import { cn } from "utils" + +import { ActionConfigBuilder } from "~/actions/_lib/ActionConfigBuilder" +import { ActionFormContext } from "~/actions/_lib/ActionForm" +import { + type AdditionalConfigFormReturn, + getTriggerByName, + humanReadableEventBase, + isTriggerWithConfig, + triggers, +} from "~/actions/_lib/triggers" +import { getTriggerConfigForm } from "~/actions/_lib/triggers/forms" +import { actions } from "~/actions/api" +import { getActionFormComponent } from "~/actions/forms" +import { isSchedulableAutomationEvent, isSequentialAutomationEvent } from "~/actions/types" +import { useCommunity } from "~/app/components/providers/CommunityProvider" +import { useUserOrThrow } from "~/app/components/providers/UserProvider" +import { entries } from "~/lib/mapping" +import { findRanksBetween } from "~/lib/rank" +import { isClientException, useServerAction } from "~/lib/serverActions" +import { addOrUpdateAutomation } from "../../../actions" +import { ConditionBlock } from "./ConditionBlock" +import { IconPicker } from "./IconPicker" +import { StagePanelActionCreator } from "./StagePanelActionCreator" + +type Props = { + currentAutomation: FullAutomation | null + stageId: StagesId + communityId: CommunitiesId + automations: FullAutomation[] + actionConfigDefaults: ActionConfigDefaultFields + onSuccess?: () => void +} + +const AutomationSelector = ({ + fieldProps, + fieldState, + label, + placeholder, + disabledAutomationId, + dataTestIdPrefix, + automations, +}: { + fieldProps: ControllerRenderProps< + CreateAutomationsSchema, + `triggers.${number}.sourceAutomationId` + > + fieldState: ControllerFieldState + label: string + placeholder: string + disabledAutomationId?: AutomationsId + dataTestIdPrefix?: string + automations: { id: AutomationsId; name: string }[] +}) => { + return ( + + {label} + + {fieldState.error && ( + {fieldState.error.message} + )} + + ) +} + +const conditionBlockSchema: z.ZodType = z.lazy(() => + z.object({ + id: z.string().optional(), + type: z.nativeEnum(AutomationConditionBlockType), + kind: z.literal("block"), + rank: z.string(), + items: z + .array( + z.union([ + z.object({ + id: z.string().optional(), + kind: z.literal("condition"), + type: z.nativeEnum(AutomationConditionType), + expression: z.string().min(1), + rank: z.string(), + }), + conditionBlockSchema, + ]) + ) + .min(1), + }) +) + +export type CreateAutomationsSchema = { + name: string + description?: string + icon?: IconConfig + condition?: ConditionBlockFormValue + triggers: { + triggerId: AutomationTriggersId + event: AutomationEvent + config?: Record + sourceAutomationId?: AutomationsId | undefined + }[] + action: { + actionInstanceId?: ActionInstancesId + action: Action + config: Record + } + conditionEvaluationTiming: ConditionEvaluationTiming +} + +type ConfigCardProps = { + icon: typeof ChevronRight + title: React.ReactNode + onRemove: () => void + children?: React.ReactNode + showCollapseToggle?: boolean + isError?: boolean + defaultCollapsed?: boolean +} + +const ConfigCard = memo( + function ConfigCard(props: ConfigCardProps) { + const [isCollapsed, setIsCollapsed] = useState(props.defaultCollapsed ?? false) + const hasContent = !!props.children + const Icon = props.icon + + return ( + setIsCollapsed(!open)} + defaultOpen={!props.defaultCollapsed} + > + + {/*
    */} + + {props.showCollapseToggle && hasContent ? ( + + + + ) : ( + <> + + + {props.title} + + + )} + + + + + {props.children} + + + + ) + }, + (prevProps, nextProps) => { + return ( + prevProps.icon === nextProps.icon && + prevProps.title === nextProps.title && + prevProps.isError === nextProps.isError && + prevProps.showCollapseToggle === nextProps.showCollapseToggle && + prevProps.defaultCollapsed === nextProps.defaultCollapsed && + prevProps.children === nextProps.children + ) + } +) + +const ConditionFieldSection = memo( + function ConditionFieldSection(props: { + form: UseFormReturn + condition: ConditionBlockFormValue | undefined + }) { + return ( + ( + +
    + Conditions (optional) + {!props.condition ? ( + + ) : ( + + )} +
    + {props.condition && ( + <> + + Define conditions that must be met for this automation to run. + Use JSONata expressions to construct a boolean value like{" "} + 'Hello' in $.pub.values.title. + + + + )} + {fieldState.error && ( + {fieldState.error.message} + )} +
    + )} + /> + ) + }, + (prevProps, nextProps) => { + return prevProps.condition === nextProps.condition + } +) + +export function StagePanelAutomationForm(props: Props) { + const schema = useMemo( + () => + z + .object({ + name: z.string().min(1, "Name is required"), + description: z.string().nullish(), + icon: z + .object({ + name: z.string(), + variant: z.enum(["solid", "outline"]).nullish(), + color: z.string().nullish(), + }) + .nullish(), + conditionEvaluationTiming: conditionEvaluationTimingSchema.nullish(), + condition: conditionBlockSchema.nullish(), + triggers: z + .array( + z.discriminatedUnion( + "event", + entries(triggers).map(([event, automation]) => + z.object({ + triggerId: automationTriggersIdSchema, + event: z.literal(event), + ...(automation.config ? { config: automation.config } : {}), + ...(isSequentialAutomationEvent(event) + ? { sourceAutomationId: automationsIdSchema } + : {}), + }) + ) as unknown as [ + z.ZodObject<{ + triggerId: typeof automationTriggersIdSchema + event: z.ZodLiteral + config: z.ZodObject + sourceAutomationId: z.ZodOptional< + z.ZodType + > + }>, + ...z.ZodObject<{ + triggerId: typeof automationTriggersIdSchema + event: z.ZodLiteral + config: z.ZodObject + sourceAutomationId: z.ZodOptional< + z.ZodType + > + }>[], + ] + ) + ) + .min(1, "At least one trigger is required"), + + action: z.discriminatedUnion( + "action", + entries(actions).map(([actionName]) => + z.object({ + actionInstanceId: actionInstancesIdSchema, + action: z.literal(actionName), + config: new ActionConfigBuilder(actionName) + .withDefaults({}) + .getSchema(), + }) + ) as [ + z.ZodObject<{ + actionInstanceId: typeof actionInstancesIdSchema + action: z.ZodLiteral + config: z.ZodObject + }>, + ...z.ZodObject<{ + actionInstanceId: typeof actionInstancesIdSchema + action: z.ZodLiteral + config: z.ZodObject + }>[], + ], + { + message: "Action is required", + errorMap: (issue, ctx) => { + if ( + issue.code === z.ZodIssueCode.invalid_union_discriminator || + !issue.message + ) { + return { message: "Action is required" } + } + + return { message: issue.message } + }, + } + ), + }) + .superRefine((data, ctx) => { + if (!data.triggers?.length) { + return + } + + for (const [idx, trigger] of data.triggers.entries()) { + if (!isSequentialAutomationEvent(trigger.event)) { + continue + } + if (!trigger.sourceAutomationId) { + ctx.addIssue({ + path: ["triggers", idx, "sourceAutomationId"], + code: z.ZodIssueCode.custom, + message: + "Source automation is required for automation chaining events", + }) + continue + } + + if (trigger.sourceAutomationId === props.currentAutomation?.id) { + ctx.addIssue({ + path: ["triggers", idx, "sourceAutomationId"], + code: z.ZodIssueCode.custom, + message: "Automations may not trigger themselves in a loop", + }) + } + } + }), + [props.currentAutomation?.id] + ) + + const runUpsertAutomation = useServerAction(addOrUpdateAutomation) + + const defaultValues = useMemo(() => { + if (!props.currentAutomation) { + return { + name: "", + description: "", + icon: undefined, + action: { + actionInstanceId: undefined, + action: undefined, + config: {}, + }, + triggers: [], + condition: undefined, + conditionEvaluationTiming: undefined, + } + } + + const actionInstance = props.currentAutomation.actionInstances[0] + + return { + name: props.currentAutomation.name, + description: props.currentAutomation.description ?? "", + icon: props.currentAutomation.icon as IconConfig | undefined, + action: { + actionInstanceId: actionInstance?.id, + action: actionInstance?.action, + config: actionInstance?.config ?? {}, + }, + triggers: props.currentAutomation.triggers.map((trigger) => ({ + triggerId: trigger.id, + event: trigger.event, + config: trigger.config, + sourceAutomationId: trigger.sourceAutomationId, + })), + conditionEvaluationTiming: props.currentAutomation.conditionEvaluationTiming, + condition: props.currentAutomation.condition, + } as CreateAutomationsSchema + }, [props.currentAutomation]) + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues, + }) + + const { setError } = form + + const { user } = useUserOrThrow() + + const onSubmit = useCallback( + async (data: CreateAutomationsSchema) => { + const result = await runUpsertAutomation({ + stageId: props.stageId, + data, + automationId: props.currentAutomation?.id as AutomationsId | undefined, + }) + if (!isClientException(result)) { + props.onSuccess?.() + return + } + + setError("root", { message: result.error }) + }, + [props.currentAutomation?.id, props.stageId, runUpsertAutomation, props.onSuccess, setError] + ) + + const formId = useId() + + const selectedAction = useWatch({ control: form.control, name: "action" }) + + // track the initial action to detect when the user changes the action type + // using a ref to avoid triggering the effect when we update the tracking value + const initialActionRef = useRef(defaultValues.action?.action) + + useEffect(() => { + if (!selectedAction?.action) { + return + } + + // if this is the first time we're seeing this action (matches initial), don't clear + if (initialActionRef.current === selectedAction.action) { + return + } + + // action type changed, clear the config and update the ref + form.setValue("action.config", {}) + initialActionRef.current = selectedAction.action + }, [selectedAction?.action, form]) + + const condition = form.watch("condition") + + const { + fields: selectedTriggers, + append: appendTrigger, + // this doesnt seem to work properly, see https://github.com/react-hook-form/react-hook-form/issues/12791 + remove: _removeTrigger, + } = useFieldArray({ + control: form.control, + name: "triggers", + }) + + const errors = form.formState.errors + + const _hasCondition = Boolean(condition) + const needsConditionEvaluationTiming = selectedTriggers.some((trigger) => + isSchedulableAutomationEvent(trigger.event) + ) + console.log(needsConditionEvaluationTiming) + + return ( +
    +
    +
    + { + return ( + + + Icon + {/* okay this is evil but i just don't want to show the label it looks bad */} + + + + + ) + }} + /> + { + return ( + + Name + + + ) + }} + /> +
    + {errors.icon && ( + Icon Error: {errors.icon.message} + )} + {errors.name && ( + Name Error: {errors.name.message} + )} +
    + + ( + + )} + /> + + {selectedTriggers.length > 0 && ( + <> + + {condition && needsConditionEvaluationTiming && ( + ( + + + When to evaluate the condition + +

    + For duration-based triggers, conditions can be + evaluated at two points: when the automation is + scheduled (eg, whenever as Pub enters the stage) and + when it actually executes (after the duration + passes). +
    +
    + "When scheduled" only schedules the automation if + the condition is met initially, useful for static + conditions like pub type. +
    +
    + "On execution" schedules all automations but checks + the condition at runtime, useful for dynamic + conditions that may change over time (like "author + has replied"). +
    +
    + "Both" is the most restrictive option, requiring the + condition to be true at both scheduling and + execution time, which minimizes unnecessary + scheduled jobs while ensuring the condition still + holds when the automation runs. +

    +
    +
    + +
    + )} + /> + )} + + )} + + {selectedTriggers.length > 0 && ( + { + return ( + + Run +
    + {field.value?.action ? ( + + ) : null} + {!field.value?.action && ( + { + form.setValue("action", { + actionInstanceId: + crypto.randomUUID() as ActionInstancesId, + action: actionName, + config: {}, + }) + }} + isSuperAdmin={user?.isSuperAdmin} + > + + + )} +
    + {fieldState.error && ( + + {fieldState.error.message} + + )} +
    + ) + }} + /> + )} + + {form.formState.errors.root && ( + {form.formState.errors.root?.message} + )} + + + + ) +} + +const TriggerConfigCard = memo( + function TriggerConfigCard(props: { + trigger: CreateAutomationsSchema["triggers"][number] + form: UseFormReturn + idx: number + community: Communities + removeTrigger: () => void + currentlyEditingAutomationId: AutomationsId | undefined + stageAutomations: { id: AutomationsId; name: string }[] + isEditing: boolean + }) { + const trigger = getTriggerByName(props.trigger.event) + + const TriggerForm = useMemo(() => { + if (!isTriggerWithConfig(props.trigger.event)) { + return null + } + + return getTriggerConfigForm(props.trigger.event) + }, [props.trigger.event]) + + if (!trigger) { + return null + } + + const hasConfig = Boolean( + isSequentialAutomationEvent(props.trigger.event) || (trigger?.config && TriggerForm) + ) + + if (!hasConfig) { + return ( + + ) + } + + return ( + + {isSequentialAutomationEvent(props.trigger.event) && ( + ( + + )} + /> + )} + {trigger.config && TriggerForm && ( + + )} + + ) + }, + (prevProps, nextProps) => { + return ( + prevProps.trigger.event === nextProps.trigger.event && + prevProps.trigger.triggerId === nextProps.trigger.triggerId && + prevProps.idx === nextProps.idx && + prevProps.isEditing === nextProps.isEditing && + prevProps.currentlyEditingAutomationId === nextProps.currentlyEditingAutomationId + ) + } +) + +function ActionConfigCardWrapper(props: { + action: Action + form: UseFormReturn + onChange: (value: { + actionInstanceId?: ActionInstancesId + action: Action | undefined + config: Record + }) => void + isEditing: boolean + defaults?: string[] +}) { + const removeAction = useCallback(() => { + props.onChange({ + actionInstanceId: undefined, + action: undefined, + config: {}, + }) + }, [props.onChange]) + + return ( + + ) +} + +const ActionConfigCard = memo( + function ActionConfigCard(props: { + action: Action + form: UseFormReturn + removeAction: () => void + isEditing: boolean + defaults?: string[] + }) { + const actionDef = actions[props.action] + const ActionFormComponent = useMemo(() => { + return getActionFormComponent(props.action) + }, [props.action]) + + const translatedDefaults = useMemo(() => { + return props.defaults?.map((key) => `action.config.${key}`) ?? [] + }, [props.defaults]) + + const translatedTokens = useMemo(() => { + return Object.entries(actionDef.tokens ?? {}).reduce((acc, [key, value]) => { + acc[`action.config.${key}`] = value + return acc + }, {} as TokenContext) + }, [actionDef.tokens]) + + if (!ActionFormComponent) { + return null + } + + return ( + { + return ( + + + as UseFormReturn, + defaultFields: translatedDefaults, + context: { + type: "automation", + }, + }} + > + + + {fieldState.error && ( + + {fieldState.error.message} + + )} + + + ) + }} + /> + ) + }, + (prevProps, nextProps) => { + return prevProps.action === nextProps.action && prevProps.isEditing === nextProps.isEditing + } +) + +export const TriggerField = (props: { + field: ControllerRenderProps + fieldState: ControllerFieldState + automations: { id: AutomationsId; name: string }[] + currentlyEditingAutomationId: AutomationsId | null + form: UseFormReturn + appendTrigger: (trigger: CreateAutomationsSchema["triggers"][number]) => void +}) => { + const community = useCommunity() + + const selectTriggers = useMemo(() => { + return Object.values(AutomationEvent) + .filter((event) => !props.field.value?.some((t) => t.event === event)) + .sort((a, b) => { + // Put "manual" trigger first + if (a === AutomationEvent.manual) return -1 + if (b === AutomationEvent.manual) return 1 + return 0 + }) + .map((event) => { + const automation = getTriggerByName(event) + + return ( + + + {humanReadableEventBase(event, community)} + + ) + }) + }, [props.field.value, community]) + + return ( + + When +
    + {props.field.value && props.field.value.length > 0 + ? props.field.value.map((field, idx) => { + return ( + ( + { + props.field.onChange( + props.field.value.filter( + (t) => t.triggerId !== field.value.triggerId + ) + ) + }} + isEditing={!!props.currentlyEditingAutomationId} + /> + )} + /> + ) + }) + : null} + +
    + {props.fieldState.error && ( + {props.fieldState.error.message} + )} +
    + ) +} diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/StagePanelAutomations.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/StagePanelAutomations.tsx new file mode 100644 index 0000000000..f885f21d24 --- /dev/null +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/StagePanelAutomations.tsx @@ -0,0 +1,59 @@ +import type { CommunitiesId, UsersId } from "db/public" +import type { FullAutomation } from "db/types" +import type { CommunityStage } from "~/lib/server/stages" + +import { Card, CardContent, CardTitle } from "ui/card" +import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "ui/empty" +import { Bot } from "ui/icon" +import { ItemGroup } from "ui/item" + +import { StagePanelCardHeader } from "../../editor/StagePanelCard" +import { AddAutomationButton } from "./AddAutomationButton" +import { StagePanelAutomation } from "./StagePanelAutomation" + +type Props = { + userId: UsersId + stage: CommunityStage + communityId: CommunitiesId + automations: FullAutomation[] +} + +export const StagePanelAutomations = (props: Props) => { + return ( + + +
    + + Automations +
    + +
    + + + {props.automations.length > 0 ? ( + props.automations.map((automation) => ( + + )) + ) : ( + + + + + + No automations + + Add an automation to get started + + + + )} + + +
    + ) +} diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/StagePanelAutomationsContent.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/StagePanelAutomationsContent.tsx new file mode 100644 index 0000000000..ca90d230ba --- /dev/null +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/StagePanelAutomationsContent.tsx @@ -0,0 +1,90 @@ +"use client" + +import type { CommunitiesId, UsersId } from "db/public" +import type { FullAutomation } from "db/types" +import type { ActionConfigDefaultFields } from "~/lib/server/actions" +import type { CommunityStage } from "~/lib/server/stages" + +import { ChevronLeft } from "lucide-react" + +import { Button } from "ui/button" +import { Card, CardContent, CardHeader, CardTitle } from "ui/card" +import { DynamicIcon } from "ui/dynamic-icon" + +import { StagePanelAutomationForm } from "./StagePanelAutomationForm" +import { StagePanelAutomations } from "./StagePanelAutomations" +import { useAutomationId } from "./useAutomationId" + +type Props = { + userId: UsersId + communityId: CommunitiesId + stage: CommunityStage + automations: FullAutomation[] + actionConfigDefaults: ActionConfigDefaultFields +} + +export function StagePanelAutomationsContent(props: Props) { + const { automationId, setAutomationId } = useAutomationId() + const currentAutomation = props.automations.find((automation) => automation.id === automationId) + + return ( +
    +
    + +
    + +
    + + + + + {currentAutomation ? ( + <> + +

    {currentAutomation.name}

    + + ) : ( +

    Add automation

    + )} +
    +
    + + setAutomationId(null)} + /> + +
    +
    +
    + ) +} diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/StagePanelAutomationsLoader.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/StagePanelAutomationsLoader.tsx new file mode 100644 index 0000000000..7c82edb698 --- /dev/null +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/StagePanelAutomationsLoader.tsx @@ -0,0 +1,42 @@ +import type { CommunitiesId, StagesId, UsersId } from "db/public" + +import { getStageAutomations } from "~/lib/db/queries" +import { getActionConfigDefaultsFields } from "~/lib/server/actions" +import { getStages } from "~/lib/server/stages" +import { StagePanelAutomationsContent } from "./StagePanelAutomationsContent" + +type Props = { + userId: UsersId + communityId: CommunitiesId + stageId: StagesId +} + +export async function StagePanelAutomationsLoader(props: Props) { + const [stage, actionConfigDefaults, automations] = await Promise.all([ + getStages( + { stageId: props.stageId, userId: props.userId, communityId: props.communityId }, + { + withAutomations: { + detail: "full", + filter: "all", + }, + } + ).executeTakeFirst(), + getActionConfigDefaultsFields(props.communityId), + getStageAutomations(props.stageId), + ]) + + if (!stage) { + return null + } + + return ( + + ) +} diff --git a/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/useAutomationId.tsx b/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/useAutomationId.tsx new file mode 100644 index 0000000000..75250b1808 --- /dev/null +++ b/core/app/c/[communitySlug]/stages/manage/components/panel/automationsTab/useAutomationId.tsx @@ -0,0 +1,19 @@ +"use client" + +import type { AutomationsId } from "db/public" + +import { parseAsString, useQueryState } from "nuqs" + +export function useAutomationId() { + const [automationId, setAutomationId] = useQueryState( + "automation-id", + parseAsString.withOptions({ + shallow: true, + }) + ) + + return { + automationId: automationId as AutomationsId | null, + setAutomationId: setAutomationId as (automationId: AutomationsId | null) => void, + } +} diff --git a/core/app/c/[communitySlug]/stages/manage/page.tsx b/core/app/c/[communitySlug]/stages/manage/page.tsx index 5620f93b09..dc98edb39f 100644 --- a/core/app/c/[communitySlug]/stages/manage/page.tsx +++ b/core/app/c/[communitySlug]/stages/manage/page.tsx @@ -94,6 +94,7 @@ export default async function Page(props: Props) { {searchParams.editingStageId && ( - + ) } diff --git a/core/app/c/[communitySlug]/types/NewTypeForm.tsx b/core/app/c/[communitySlug]/types/NewTypeForm.tsx index 27e062a897..4cf11354b8 100644 --- a/core/app/c/[communitySlug]/types/NewTypeForm.tsx +++ b/core/app/c/[communitySlug]/types/NewTypeForm.tsx @@ -2,7 +2,6 @@ import type { Static } from "@sinclair/typebox" import type { PubFieldsId, PubTypesId } from "db/public" import type { ReactNode } from "react" import type { FieldValues, UseFormReturn } from "react-hook-form" -import type { PubFieldContext } from "ui/pubFields" import { useCallback, useMemo } from "react" import { typeboxResolver } from "@hookform/resolvers/typebox" @@ -21,7 +20,7 @@ import { } from "ui/form" import { Input } from "ui/input" import { MultiSelect } from "ui/multi-select" -import { usePubFieldContext } from "ui/pubFields" +import { type PubFieldContext, usePubFieldContext } from "ui/pubFields" import { toast } from "ui/use-toast" import { useCommunity } from "~/app/components/providers/CommunityProvider" @@ -172,7 +171,7 @@ export const NewTypeForm = ({ /> {props.mode === "create" && } {form.formState.errors.root && ( -
    +
    {form.formState.errors.root.message}
    )} diff --git a/core/app/c/[communitySlug]/types/[pubTypeId]/edit/FieldBlock.tsx b/core/app/c/[communitySlug]/types/[pubTypeId]/edit/FieldBlock.tsx index 5ab805234e..055cae4333 100644 --- a/core/app/c/[communitySlug]/types/[pubTypeId]/edit/FieldBlock.tsx +++ b/core/app/c/[communitySlug]/types/[pubTypeId]/edit/FieldBlock.tsx @@ -55,7 +55,7 @@ export const FieldBlock = ({ type="button" disabled={isDisabled} variant="ghost" - className="p-2 opacity-0 hover:bg-white group-focus-within:opacity-100 group-hover:opacity-100 [&_svg]:pointer-events-auto [&_svg]:hover:text-red-500" + className="p-2 opacity-0 hover:bg-white group-focus-within:opacity-100 group-hover:opacity-100 [&_svg]:pointer-events-auto [&_svg]:hover:text-destructive" aria-label={`Restore ${field.name}`} onClick={() => { restoreElement(index) @@ -75,7 +75,7 @@ export const FieldBlock = ({ type="button" disabled={isDisabled} variant="ghost" - className="p-2 opacity-0 hover:bg-white group-focus-within:opacity-100 group-hover:opacity-100 [&_svg]:pointer-events-auto [&_svg]:hover:text-red-500" + className="p-2 opacity-0 hover:bg-white group-focus-within:opacity-100 group-hover:opacity-100 [&_svg]:pointer-events-auto [&_svg]:hover:text-destructive" aria-label={`Delete ${field.name}`} data-testid={`delete-${field.name}`} onClick={() => { diff --git a/core/app/components/ActionUI/ActionConfigForm.tsx b/core/app/components/ActionUI/ActionConfigForm.tsx deleted file mode 100644 index 08952ed95d..0000000000 --- a/core/app/components/ActionUI/ActionConfigForm.tsx +++ /dev/null @@ -1,113 +0,0 @@ -"use client" - -import type { ActionInstances, ActionInstancesId, StagesId } from "db/public" -import type { z } from "zod" - -import { startTransition, useCallback, useMemo } from "react" - -import { TokenProvider } from "ui/tokens" -import { toast } from "ui/use-toast" - -import { ActionConfigBuilder } from "~/actions/_lib/ActionConfigBuilder" -import { ActionForm } from "~/actions/_lib/ActionForm" -import { getActionByName } from "~/actions/api" -import { getActionFormComponent } from "~/actions/forms" -import { deleteAction, updateAction } from "~/app/c/[communitySlug]/stages/manage/actions" -import { didSucceed, useServerAction } from "~/lib/serverActions" - -export type Props = { - actionInstance: ActionInstances - stageId: StagesId - defaultFields: string[] -} - -export const ActionConfigForm = (props: Props) => { - const action = getActionByName(props.actionInstance.action) - const runDeleteAction = useServerAction(deleteAction) - - const onDelete = useCallback(async () => { - const result = await runDeleteAction( - props.actionInstance.id as ActionInstancesId, - props.stageId - ) - if (didSucceed(result)) { - toast({ - title: "Action deleted successfully!", - }) - } - }, [props.actionInstance.id, props.stageId, runDeleteAction]) - - const schema = useMemo(() => { - const config = new ActionConfigBuilder(action.name) - .withConfig(props.actionInstance.config ?? {}) - .withDefaults(props.defaultFields) - .getSchema() - return config - }, [action.name, props.actionInstance.config, props.defaultFields]) - - const runUpdateAction = useServerAction(updateAction) - - const onSubmit = useCallback( - async (values: z.infer>) => { - startTransition(async () => { - const result = await runUpdateAction( - props.actionInstance.id as ActionInstancesId, - props.stageId, - { - config: values, - } - ) - - if (result && "success" in result) { - toast({ - title: "Action updated successfully!", - variant: "default", - // TODO: SHOULD ABSOLUTELY BE SANITIZED - description: ( -
    - ), - }) - } - - if (result && "success" in result) { - toast({ - title: "Action updated successfully!", - variant: "default", - // TODO: SHOULD ABSOLUTELY BE SANITIZED - description: ( -
    - ), - }) - } - }) - }, - [runUpdateAction, props.actionInstance.id, props.stageId] - ) - - const ActionFormComponent = getActionFormComponent(action.name) - - return ( - - - - - - ) -} diff --git a/core/app/components/ActionUI/ActionRunForm.tsx b/core/app/components/ActionUI/ActionRunForm.tsx deleted file mode 100644 index f997cf4651..0000000000 --- a/core/app/components/ActionUI/ActionRunForm.tsx +++ /dev/null @@ -1,143 +0,0 @@ -"use client" - -import type { ActionInstances, CommunitiesId, PubsId } from "db/public" -import type { UseFormReturn } from "react-hook-form" - -import { Suspense, useCallback, useState } from "react" - -import { logger } from "logger" -import { Button } from "ui/button" -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "ui/dialog" -import { Separator } from "ui/separator" -import { TokenProvider } from "ui/tokens" -import { toast } from "ui/use-toast" - -import { ActionForm } from "~/actions/_lib/ActionForm" -import { getActionByName } from "~/actions/api" -import { runActionInstance } from "~/actions/api/serverAction" -import { getActionFormComponent } from "~/actions/forms" -import { SkeletonCard } from "~/app/components/skeletons/SkeletonCard" -import { useServerAction } from "~/lib/serverActions" -import { useCommunity } from "../providers/CommunityProvider" - -type Props = { - actionInstance: ActionInstances - pubId: PubsId - defaultFields: string[] -} - -export const ActionRunForm = (props: Props) => { - const action = getActionByName(props.actionInstance.action) - const ActionFormComponent = getActionFormComponent(action.name) - const community = useCommunity() - const runAction = useServerAction(runActionInstance) - - const onSubmit = useCallback( - async (values: Record, form: UseFormReturn) => { - const result = await runAction({ - actionInstanceId: props.actionInstance.id, - pubId: props.pubId, - actionInstanceArgs: values, - communityId: community.id as CommunitiesId, - stack: [], - }) - - if ("success" in result) { - toast({ - title: - "title" in result && typeof result.title === "string" - ? result.title - : `Successfully ran ${props.actionInstance.name || action.name}`, - variant: "default", - description: ( -
    {result.report}
    - ), - }) - return - } - if ("issues" in result && result.issues) { - const issues = result.issues - for (const issue of issues) { - form.setError(issue.path.join("."), { - message: issue.message, - }) - } - } - - form.setError("root.serverError", { - message: result.error, - }) - }, - [ - runAction, - props.actionInstance.id, - props.pubId, - action.name, - community.id, - props.actionInstance.name, - ] - ) - - const [open, setOpen] = useState(false) - - const onClose = useCallback(() => { - setOpen(false) - }, []) - - if (!action) { - logger.info(`Invalid action name ${props.actionInstance.action}`) - return null - } - - return ( - - - - - - - -
    - - - {props.actionInstance.name || action.name} - -
    - -
    -
    - - }> - - - -
    -
    -
    -
    - ) -} diff --git a/core/app/components/AutomationUI/ActionRunResult.tsx b/core/app/components/AutomationUI/ActionRunResult.tsx new file mode 100644 index 0000000000..2f67ad1fed --- /dev/null +++ b/core/app/components/AutomationUI/ActionRunResult.tsx @@ -0,0 +1,154 @@ +"use client" + +import type { ActionRuns } from "db/public" + +import { useState } from "react" +import { ChevronDown, ChevronRight } from "lucide-react" + +import { Badge } from "ui/badge" +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "ui/collapsible" +import { cn } from "utils" + +import { isActionFailure, isActionSuccess } from "~/actions/results" + +type Props = { + actionRun: ActionRuns + className?: string +} + +export const ActionRunResult = ({ actionRun, className }: Props) => { + const [showConfig, setShowConfig] = useState(false) + const [showCause, setShowCause] = useState(false) + const [showData, setShowData] = useState(false) + + const result = actionRun.result + + if (!result || typeof result !== "object") { + return ( +
    +
    +					{JSON.stringify(result, null, 2)}
    +				
    +
    + ) + } + + const isSuccess = isActionSuccess(result) + const isFailure = isActionFailure(result) + + return ( +
    + {isSuccess && ( +
    + {result.title && ( +
    {result.title}
    + )} + {result.report && ( +
    + {typeof result.report === "string" ?

    {result.report}

    : null} +
    + )} + {!!result.data && + typeof result.data === "object" && + Object.keys(result.data).length > 0 && ( + + + + + +
    +										{JSON.stringify(result.data, null, 2)}
    +									
    +
    +
    + )} +
    + )} + + {isFailure && ( +
    + {result.title && ( +
    {result.title}
    + )} +
    {result.error}
    + {!!result.cause && ( + + + + + +
    +									{typeof result.cause === "string"
    +										? result.cause
    +										: JSON.stringify(result.cause, null, 2)}
    +								
    +
    +
    + )} +
    + )} + + {!isSuccess && !isFailure && ( +
    +					{JSON.stringify(result, null, 2)}
    +				
    + )} + + {"config" in result && !!result.config && ( + + + + + +
    +							{JSON.stringify(result.config, null, 2)}
    +						
    +
    +
    + )} +
    + ) +} + +export const ActionRunStatusBadge = ({ status }: { status: ActionRuns["status"] }) => { + switch (status) { + case "success": + return success + case "failure": + return failure + case "scheduled": + return ( + + scheduled + + ) + default: + return unknown + } +} diff --git a/core/app/components/AutomationUI/AutomationRunForm.tsx b/core/app/components/AutomationUI/AutomationRunForm.tsx new file mode 100644 index 0000000000..3f4fec26b6 --- /dev/null +++ b/core/app/components/AutomationUI/AutomationRunForm.tsx @@ -0,0 +1,222 @@ +"use client" + +import type { CommunitiesId, PubsId } from "db/public" +import type { FullAutomation } from "db/types" +import type { LucideIcon } from "lucide-react" +import type { FieldValues, UseFormReturn } from "react-hook-form" + +import { Suspense, useCallback, useState } from "react" + +import { logger } from "logger" +import { Button } from "ui/button" +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "ui/dialog" +import { DynamicIcon, type IconConfig } from "ui/dynamic-icon" +import { Item, ItemContent, ItemHeader } from "ui/item" +import { Separator } from "ui/separator" +import { FormSubmitButton, FormSubmitButtonWithDropdown } from "ui/submit-button" +import { TokenProvider } from "ui/tokens" +import { toast } from "ui/use-toast" + +import { ActionForm } from "~/actions/_lib/ActionForm" +import { getActionByName } from "~/actions/api" +import { runAutomationManual } from "~/actions/api/serverAction" +import { getActionFormComponent } from "~/actions/forms" +import { isActionSuccess } from "~/actions/results" +import { useServerAction } from "~/lib/serverActions" +import { useCommunity } from "../providers/CommunityProvider" +import { SkeletonCard } from "../skeletons/SkeletonCard" + +type ActionCardProps = { + icon: LucideIcon + title: string + children?: React.ReactNode +} + +const ActionCard = (props: ActionCardProps) => { + const Icon = props.icon + + return ( + + + + {props.title} + + {props.children && ( + {props.children} + )} + + ) +} + +type Props = { + automation: FullAutomation + pubId: PubsId + canOverrideAutomationConditions: boolean +} + +export const AutomationRunForm = (props: Props) => { + const mainActionInstance = props.automation.actionInstances[0] + const action = getActionByName(mainActionInstance.action) + const ActionFormComponent = getActionFormComponent(action.name) + const community = useCommunity() + const runAutomation = useServerAction(runAutomationManual) + + const hasConditions = Boolean(props.automation.condition) + const showSkipConditionsOption = props.canOverrideAutomationConditions && hasConditions + + const onSubmit = useCallback( + async ( + values: Record, + form: UseFormReturn, + options?: Record + ) => { + const skipConditionCheck = Boolean(options?.skipConditions) + + const result = await runAutomation({ + automationId: props.automation.id, + pubId: props.pubId, + manualActionInstancesOverrideArgs: { + [mainActionInstance.id]: values, + }, + communityId: community.id as CommunitiesId, + stack: [], + skipConditionCheck: skipConditionCheck && hasConditions, + }) + + if (isActionSuccess(result)) { + toast({ + title: + "title" in result && typeof result.title === "string" + ? result.title + : `Successfully ran ${props.automation.name || action.niceName}`, + variant: "default", + description: ( +
    + {result.report?.report} +
    + ), + }) + return + } + if ("issues" in result && result.issues) { + const issues = result.issues + for (const issue of issues) { + form.setError(issue.path.join("."), { + message: issue.message, + }) + } + } + + form.setError("root.serverError", { + message: result.error, + }) + }, + [ + runAutomation, + props.automation.id, + props.automation.name, + props.pubId, + mainActionInstance.id, + community.id, + action.niceName, + hasConditions, + ] + ) + + const [open, setOpen] = useState(false) + + const onClose = useCallback(() => { + setOpen(false) + }, []) + + if (!action) { + logger.info( + `Invalid action name for automation ${props.automation.name}: $mainActionInstance.action` + ) + return null + } + + const automationIcon = props.automation.icon + + return ( + + + + + + + +
    + + + {props.automation.name} + +
    + +
    +
    + + + showSkipConditionsOption ? ( + + submit({ skipConditions: true }), + }, + ]} + /> + ) : ( + + ) + } + secondaryButton={{ + text: "Cancel", + onClick: onClose, + }} + context={{ type: "run", pubId: props.pubId }} + > + }> + + + + +
    +
    +
    +
    + ) +} diff --git a/core/app/components/AutomationUI/AutomationRunResult.tsx b/core/app/components/AutomationUI/AutomationRunResult.tsx new file mode 100644 index 0000000000..13a4cc47e7 --- /dev/null +++ b/core/app/components/AutomationUI/AutomationRunResult.tsx @@ -0,0 +1,126 @@ +"use client" + +import type { ActionRuns, AutomationRuns } from "db/public" + +import { useState } from "react" +import { ChevronDown, ChevronRight } from "lucide-react" + +import { Badge } from "ui/badge" +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "ui/collapsible" +import { cn } from "utils" + +import { type AutomationRunComputedStatus, getAutomationRunStatus } from "~/actions/results" +import { ActionRunResult, ActionRunStatusBadge } from "./ActionRunResult" + +type Props = { + automationRun: AutomationRuns & { actionRuns: ActionRuns[] } + className?: string + showOverallStatus?: boolean +} + +export const AutomationRunResult = ({ + automationRun, + className, + showOverallStatus = false, +}: Props) => { + const [expandedActionRuns, setExpandedActionRuns] = useState>(new Set()) + + const status = getAutomationRunStatus(automationRun) + + const toggleActionRun = (actionRunId: string) => { + const newExpanded = new Set(expandedActionRuns) + if (newExpanded.has(actionRunId)) { + newExpanded.delete(actionRunId) + } else { + newExpanded.add(actionRunId) + } + setExpandedActionRuns(newExpanded) + } + + if (!automationRun.actionRuns || automationRun.actionRuns.length === 0) { + return
    No action runs found
    + } + + return ( +
    + {showOverallStatus && ( +
    + Overall Status: + +
    + )} + + {automationRun.actionRuns.length === 1 ? ( + + ) : ( +
    + {automationRun.actionRuns.map((actionRun, index) => { + const isExpanded = expandedActionRuns.has(actionRun.id) + return ( + toggleActionRun(actionRun.id)} + > +
    + + + + + + +
    +
    + ) + })} +
    + )} +
    + ) +} + +export const AutomationRunStatusBadge = ({ status }: { status: AutomationRunComputedStatus }) => { + const getStatusVariant = () => { + switch (status) { + case "success": + return "default" + case "failure": + return "destructive" + case "scheduled": + return "secondary" + case "partial": + return "outline" + default: + return "outline" + } + } + + return ( + + {status} + + ) +} diff --git a/core/app/components/ActionUI/PubsRunActionDropDownMenuItem.tsx b/core/app/components/AutomationUI/PubsRunActionDropDownMenuItem.tsx similarity index 100% rename from core/app/components/ActionUI/PubsRunActionDropDownMenuItem.tsx rename to core/app/components/AutomationUI/PubsRunActionDropDownMenuItem.tsx diff --git a/core/app/components/ActionUI/PubsRunActionDropDownMenu.tsx b/core/app/components/AutomationUI/PubsRunAutomationDropDownMenu.tsx similarity index 64% rename from core/app/components/ActionUI/PubsRunActionDropDownMenu.tsx rename to core/app/components/AutomationUI/PubsRunAutomationDropDownMenu.tsx index eec9832e83..299ba7406e 100644 --- a/core/app/components/ActionUI/PubsRunActionDropDownMenu.tsx +++ b/core/app/components/AutomationUI/PubsRunAutomationDropDownMenu.tsx @@ -1,34 +1,36 @@ import "server-only" import type { PubsId } from "db/public" +import type { FullAutomation } from "db/types" import type { ButtonProps } from "ui/button" -import type { ActionInstanceWithConfigDefaults } from "~/lib/types" import { Button } from "ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "ui/dropdown-menu" import { ChevronDown, Play } from "ui/icon" import { cn } from "utils" -import { ActionRunForm } from "./ActionRunForm" +import { AutomationRunForm } from "./AutomationRunForm" -export type PubsRunActionDropDownMenuProps = { - actionInstances: ActionInstanceWithConfigDefaults[] +export type PubsRunAutomationDropDownMenuProps = { + automations: FullAutomation[] pubId: PubsId testId?: string /* accessible text for the button */ buttonText?: string iconOnly?: boolean + canOverrideAutomationConditions: boolean } & ButtonProps -export const PubsRunActionDropDownMenu = async ({ - actionInstances, +export const PubsRunAutomationsDropDownMenu = async ({ pubId, testId, iconOnly, buttonText, + automations, + canOverrideAutomationConditions, ...buttonProps -}: PubsRunActionDropDownMenuProps) => { - if (!actionInstances.length) { +}: PubsRunAutomationDropDownMenuProps) => { + if (!automations.length) { return null } @@ -50,12 +52,12 @@ export const PubsRunActionDropDownMenu = async ({ - {actionInstances.map((actionInstance) => ( - ( + ))} diff --git a/core/app/components/ActionUI/defaultFieldConfig.ts b/core/app/components/AutomationUI/defaultFieldConfig.ts similarity index 100% rename from core/app/components/ActionUI/defaultFieldConfig.ts rename to core/app/components/AutomationUI/defaultFieldConfig.ts diff --git a/core/app/components/DataTable/DataTable.tsx b/core/app/components/DataTable/DataTable.tsx index f007ad474a..bbc0444aeb 100644 --- a/core/app/components/DataTable/DataTable.tsx +++ b/core/app/components/DataTable/DataTable.tsx @@ -198,6 +198,8 @@ export function DataTable({ onClick={(evt) => { handleRowClick(evt, row) }} + data-testid={`data-table-row-${row.id}`} + // data-testid={getRowId?.(row.original)} className={cn({ "cursor-pointer": Boolean(onRowClick), "bg-gray-100/50": striped && idx % 2, diff --git a/core/app/components/EllipsisMenu.tsx b/core/app/components/EllipsisMenu.tsx index 8f441ccf27..881df53cbd 100644 --- a/core/app/components/EllipsisMenu.tsx +++ b/core/app/components/EllipsisMenu.tsx @@ -3,8 +3,8 @@ import type { ReactNode } from "react" import type { ButtonProps } from "ui/button" -import { forwardRef } from "react" -import { MoreHorizontal } from "lucide-react" +import { createContext, forwardRef, useContext, useState } from "react" +import { MoreHorizontal, MoreVertical } from "lucide-react" import { Button } from "ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from "ui/dropdown-menu" @@ -33,30 +33,53 @@ interface EllipsisMenuProps { sideOffset?: number triggerSize?: "sm" | "default" | "lg" | "icon" disabled?: boolean + /** + * use horizontal if the menu represents a truncation of a list of other options, use vertical if the menu is the only list of options + * @default "vertical" + */ + orientation?: "horizontal" | "vertical" } /** * menu item that renders as a button. need forwardRef to pass on asChild * or else the menu won't close when the button is clicked */ -export const EllipsisMenuButton = forwardRef( - ({ children, className, ...props }, ref) => { - return ( - - ) +export const EllipsisMenuButton = forwardRef< + HTMLButtonElement, + ButtonProps & { + closeOnClick?: boolean } -) +>(({ children, className, onClick, closeOnClick = true, ...props }, ref) => { + const { setOpen } = useContext(EllipsisMenuContext) + return ( + + ) +}) EllipsisMenuButton.displayName = "EllipsisMenuButton" +export const EllipsisMenuContext = createContext<{ + open: boolean + setOpen: (open: boolean) => void +}>({ + open: false, + setOpen: () => {}, +}) + export const EllipsisMenu = ({ children, triggerClassName, @@ -65,33 +88,43 @@ export const EllipsisMenu = ({ side = "bottom", sideOffset = 4, triggerSize = "sm", + orientation = "vertical", disabled = false, }: EllipsisMenuProps) => { + const [open, setOpen] = useState(false) + return ( - - - + + - Open menu - - - - - {children} - - + {children} + + + ) } diff --git a/core/app/components/FormBuilder/ElementPanel/ComponentConfig/ColorPicker.tsx b/core/app/components/FormBuilder/ElementPanel/ComponentConfig/ColorPicker.tsx index 396fa7a3e7..07c66cb627 100644 --- a/core/app/components/FormBuilder/ElementPanel/ComponentConfig/ColorPicker.tsx +++ b/core/app/components/FormBuilder/ElementPanel/ComponentConfig/ColorPicker.tsx @@ -108,7 +108,7 @@ export const FormBuilderColorPickerPopover = ({ type="button" variant="ghost" size="icon" - className="ml-1 h-7 w-7 p-0 hover:bg-gray-200 hover:text-red-500" + className="ml-1 h-7 w-7 p-0 hover:bg-gray-200 hover:text-destructive" onClick={() => { parentField.onChange(parentField.value?.filter((_, i) => i !== idx)) }} diff --git a/core/app/components/FormBuilder/FormElement.tsx b/core/app/components/FormBuilder/FormElement.tsx index 13df7599e8..466f3c5ff4 100644 --- a/core/app/components/FormBuilder/FormElement.tsx +++ b/core/app/components/FormBuilder/FormElement.tsx @@ -52,7 +52,7 @@ export const FormElement = ({ element, index, isEditing, isDisabled }: FormEleme type="button" disabled={isDisabled} variant="ghost" - className="p-2 opacity-0 hover:bg-white group-focus-within:opacity-100 group-hover:opacity-100 [&_svg]:pointer-events-auto [&_svg]:hover:text-red-500" + className="p-2 opacity-0 hover:bg-white group-focus-within:opacity-100 group-hover:opacity-100 [&_svg]:pointer-events-auto [&_svg]:hover:text-destructive" aria-label={`Restore ${labelName}`} onClick={() => { restoreElement(index) @@ -67,7 +67,7 @@ export const FormElement = ({ element, index, isEditing, isDisabled }: FormEleme type="button" disabled={isDisabled} variant="ghost" - className="p-2 opacity-0 hover:bg-white group-focus-within:opacity-100 group-hover:opacity-100 [&_svg]:pointer-events-auto [&_svg]:hover:text-red-500" + className="p-2 opacity-0 hover:bg-white group-focus-within:opacity-100 group-hover:opacity-100 [&_svg]:pointer-events-auto [&_svg]:hover:text-destructive" aria-label={`Delete ${labelName}`} data-testid={`delete-${labelName}`} onClick={() => { @@ -178,7 +178,7 @@ export const FieldInputElement = ({ element, isEditing, labelId }: FieldInputEle })} > {(element.config as any)?.label ?? field.name} - {element.required && * } + {element.required && * }
    diff --git a/core/app/components/MemberSelect/MemberSelectClient.tsx b/core/app/components/MemberSelect/MemberSelectClient.tsx index afe5f56618..e2257185e3 100644 --- a/core/app/components/MemberSelect/MemberSelectClient.tsx +++ b/core/app/components/MemberSelect/MemberSelectClient.tsx @@ -4,7 +4,7 @@ import type { Communities, CommunityMembershipsId } from "db/public" import type { Option } from "ui/autocomplete" import type { MemberSelectUser, MemberSelectUserWithMembership } from "./types" -import { useCallback, useMemo, useState } from "react" +import { memo, useCallback, useMemo, useState } from "react" import { useDebouncedCallback } from "use-debounce" import { MemberRole } from "db/public" @@ -55,89 +55,98 @@ type Props = { onUserAdded: () => void } -export function MemberSelectClient({ - community, - name, - member, - users, - onChangeSearch, - onChangeValue, - onUserAdded, -}: Props) { - const options = useMemo(() => users.map(makeOptionFromUser), [users]) - const runAddMember = useServerAction(addMember) - const formElementToggle = useFormElementToggleContext() - const isEnabled = formElementToggle.isEnabled(name) +export const MemberSelectClient = memo( + function MemberSelectClient({ + community, + name, + member, + users, + onChangeSearch, + onChangeValue, + onUserAdded, + }: Props) { + const options = useMemo(() => users.map(makeOptionFromUser), [users]) + const runAddMember = useServerAction(addMember) + const formElementToggle = useFormElementToggleContext() + const isEnabled = formElementToggle.isEnabled(name) - // Force a re-mount of the element when the - // autocomplete dropdown is closed. - const [addUserButtonKey, setAddUserButtonKey] = useState(0) - const resetAddUserButton = useCallback(() => { - setAddUserButtonKey((x) => x + 1) - }, []) + // Force a re-mount of the element when the + // autocomplete dropdown is closed. + const [addUserButtonKey, setAddUserButtonKey] = useState(0) + const resetAddUserButton = useCallback(() => { + setAddUserButtonKey((x) => x + 1) + }, []) - const [selectedUser, setSelectedUser] = useState(member) + const [selectedUser, setSelectedUser] = useState(member) - const [inputValue, setInputValue] = useState(selectedUser?.email ?? "") + const [inputValue, setInputValue] = useState(selectedUser?.email ?? "") - const updateSearch = useDebouncedCallback((value: string) => { - onChangeSearch(value) - }, 400) + const updateSearch = useDebouncedCallback((value: string) => { + onChangeSearch(value) + }, 400) - const onInputValueChange = (value: string) => { - setInputValue(value) - updateSearch(value) - } + const onInputValueChange = (value: string) => { + setInputValue(value) + updateSearch(value) + } - const unsetUser = () => { - setSelectedUser(undefined) - onChangeValue(undefined) - } - const selectedUserOption = selectedUser && makeOptionFromUser(selectedUser) - return ( - - } - onInputValueChange={(val) => { - if (val === "") { - unsetUser() + const unsetUser = () => { + setSelectedUser(undefined) + onChangeValue(undefined) + } + const selectedUserOption = selectedUser && makeOptionFromUser(selectedUser) + return ( + } - onInputValueChange(val) - }} - onValueChange={async (option) => { - const user = users.find((user) => user.id === option.value) - if (!user) { - return - } - if (isMemberSelectUserWithMembership(user)) { - setSelectedUser(user) - onChangeValue(user.member.id) - } else { - const result = await runAddMember({ - userId: user.id, - role: MemberRole.contributor, - forms: [], - }) - if (didSucceed(result)) { - const member = expect(result.member) - setSelectedUser({ ...user, member }) - onChangeValue(member.id) + onInputValueChange={(val) => { + if (val === "") { + unsetUser() } - } - }} - onClose={resetAddUserButton} - icon={selectedUser ? : null} - onClear={selectedUser ? unsetUser : undefined} - /> - ) -} + onInputValueChange(val) + }} + onValueChange={async (option) => { + const user = users.find((user) => user.id === option.value) + if (!user) { + return + } + if (isMemberSelectUserWithMembership(user)) { + setSelectedUser(user) + onChangeValue(user.member.id) + } else { + const result = await runAddMember({ + userId: user.id, + role: MemberRole.contributor, + forms: [], + }) + if (didSucceed(result)) { + const member = expect(result.member) + setSelectedUser({ ...user, member }) + onChangeValue(member.id) + } + } + }} + onClose={resetAddUserButton} + icon={selectedUser ? : null} + onClear={selectedUser ? unsetUser : undefined} + /> + ) + }, + (prevProps, nextProps) => { + return ( + prevProps.name === nextProps.name && + prevProps.member?.id === nextProps.member?.id && + prevProps.users === nextProps.users + ) + } +) diff --git a/core/app/components/MemberSelect/MemberSelectClientFetch.tsx b/core/app/components/MemberSelect/MemberSelectClientFetch.tsx index 5226b7c2f3..ce347182cb 100644 --- a/core/app/components/MemberSelect/MemberSelectClientFetch.tsx +++ b/core/app/components/MemberSelect/MemberSelectClientFetch.tsx @@ -3,7 +3,7 @@ import type { Communities, CommunityMembershipsId } from "db/public" import type { MemberSelectUserWithMembership } from "./types" -import { useEffect, useState } from "react" +import { memo, useEffect, useState } from "react" import { skipToken } from "@tanstack/react-query" import { client } from "~/lib/api" @@ -34,7 +34,11 @@ const useMemberSelectData = ({ // User suggestions query const shouldQueryForUsers = !!email && email !== "" - const usersQuery = { limit: 1, communityId: community.id, email: email ?? "" } + const usersQuery = { + limit: 1, + communityId: community.id, + email: email ?? "", + } const { data: userSuggestionsResult, isPending: userSuggestionsPending, @@ -63,7 +67,12 @@ const useMemberSelectData = ({ } }, [userPending, userSuggestionsPending, shouldQueryForIndividualUser, shouldQueryForUsers]) - return { initialized, user, users: userSuggestionsResult?.body ?? [], refetchUsers: refetch } + return { + initialized, + user, + users: userSuggestionsResult?.body ?? [], + refetchUsers: refetch, + } } type Props = { @@ -72,24 +81,29 @@ type Props = { onChange: (value: CommunityMembershipsId | undefined) => void } -export function MemberSelectClientFetch({ name, value, onChange: onChangeProp }: Props) { - const community = useCommunity() - const [search, setSearch] = useState("") - const { user, users, refetchUsers } = useMemberSelectData({ - community, - memberId: value, - email: search, - }) +export const MemberSelectClientFetch = memo( + function MemberSelectClientFetch({ name, value, onChange: onChangeProp }: Props) { + const community = useCommunity() + const [search, setSearch] = useState("") + const { user, users, refetchUsers } = useMemberSelectData({ + community, + memberId: value, + email: search, + }) - return ( - - ) -} + return ( + + ) + }, + (prevProps, nextProps) => { + return prevProps.name === nextProps.name && prevProps.value === nextProps.value + } +) diff --git a/core/app/components/forms/elements/ColorPickerElement.tsx b/core/app/components/forms/elements/ColorPickerElement.tsx index 7133ce5027..21e062dfc9 100644 --- a/core/app/components/forms/elements/ColorPickerElement.tsx +++ b/core/app/components/forms/elements/ColorPickerElement.tsx @@ -47,7 +47,7 @@ export const ColorPickerPopover = ({ @@ -68,8 +68,7 @@ export const ColorPickerElement = ({ config, }: ElementProps) => { const { control } = useFormContext() - const formElementToggle = useFormElementToggleContext() - const _isEnabled = formElementToggle.isEnabled(slug) + const _formElementToggle = useFormElementToggleContext() Value.Default(colorPickerConfigSchema, config) if (!Value.Check(colorPickerConfigSchema, config)) { diff --git a/core/app/components/forms/elements/FileUploadElement.tsx b/core/app/components/forms/elements/FileUploadElement.tsx index f7699bf443..af30c87d73 100644 --- a/core/app/components/forms/elements/FileUploadElement.tsx +++ b/core/app/components/forms/elements/FileUploadElement.tsx @@ -47,11 +47,10 @@ export const FileUploadElement = ({ const runDelete = useServerAction(deleteFile) const { form, mode } = usePubForm() - const { control, getValues, setValue } = useFormContext() + const { control } = useFormContext() const formElementToggle = useFormElementToggleContext() const isEnabled = formElementToggle.isEnabled(slug) - const _files = getValues()[slug] const handleDeleteFile = useCallback( async ( diff --git a/core/app/components/forms/elements/RelatedPubsElement.tsx b/core/app/components/forms/elements/RelatedPubsElement.tsx index 0258a949b0..2f2d2570ce 100644 --- a/core/app/components/forms/elements/RelatedPubsElement.tsx +++ b/core/app/components/forms/elements/RelatedPubsElement.tsx @@ -1,6 +1,5 @@ "use client" -import type { DragEndEvent } from "@dnd-kit/core" import type { ProcessedPub } from "contracts" import type { InputComponent, PubsId, PubValuesId } from "db/public" import type { FieldErrors } from "react-hook-form" @@ -14,7 +13,14 @@ import type { } from "../types" import { useCallback, useId, useMemo, useState } from "react" -import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors } from "@dnd-kit/core" +import { + DndContext, + type DragEndEvent, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, +} from "@dnd-kit/core" import { restrictToParentElement, restrictToVerticalAxis } from "@dnd-kit/modifiers" import { SortableContext, @@ -84,7 +90,7 @@ const RelatedPubBlock = ({
    ) } +// submit button with dropdown + +type SubmitButtonWithDropdownProps = BaseSubmitButtonProps & + UseSubmitButtonStateProps & { + dropdownOptions: DropdownOption[] + } + +export const SubmitButtonWithDropdown = ({ + state, + isSubmitting, + isSubmitSuccessful, + isSubmitError, + idleText = "Submit", + pendingText = "Submitting...", + successText = "Success!", + errorText = "Error", + className = "", + onClick, + type = "submit", + dropdownOptions, + ...props +}: SubmitButtonWithDropdownProps) => { + const { buttonState, variant, isDisabled } = useSubmitButtonState({ + state, + isSubmitting, + isSubmitSuccessful, + isSubmitError, + } as UseSubmitButtonStateProps) + + return ( + + + + + + + + {dropdownOptions.map((option) => ( + + {option.label} + + ))} + + + + ) +} + +// form submit button + +type FormSubmitButtonProps = BaseSubmitButtonProps & { + formState: FormState +} + export const FormSubmitButton = ({ formState, + idleText = "Submit", + pendingText = "Submitting...", + successText = "Success!", + errorText = "Error", + className = "", + onClick, + type = "submit", ...props -}: ButtonProps & Omit & { formState: FormState }) => { +}: FormSubmitButtonProps) => { const hasErrors = Object.keys(formState.errors ?? {}).length > 0 + const isSubmitting = Boolean(formState.isSubmitting) + const isSubmitSuccessful = Boolean(formState.isSubmitSuccessful) && !hasErrors + + const { buttonState, variant, isDisabled } = useSubmitButtonState({ + isSubmitting, + isSubmitSuccessful, + isSubmitError: hasErrors, + }) + return ( - + > + {getButtonIcon(buttonState)} + {getButtonText(buttonState, { + idle: idleText, + pending: pendingText, + success: successText, + error: errorText, + })} + + ) +} + +// form submit button with dropdown + +type FormSubmitButtonWithDropdownProps = BaseSubmitButtonProps & { + formState: FormState + dropdownOptions: DropdownOption[] +} + +export const FormSubmitButtonWithDropdown = ({ + formState, + dropdownOptions, + idleText = "Submit", + pendingText = "Submitting...", + successText = "Success!", + errorText = "Error", + className = "", + onClick, + type = "submit", + ...props +}: FormSubmitButtonWithDropdownProps) => { + const hasErrors = Object.keys(formState.errors ?? {}).length > 0 + const isSubmitting = Boolean(formState.isSubmitting) + const isSubmitSuccessful = Boolean(formState.isSubmitSuccessful) && !hasErrors + + const { buttonState, variant, isDisabled } = useSubmitButtonState({ + isSubmitting, + isSubmitSuccessful, + isSubmitError: hasErrors, + }) + + return ( + + + + + + + + {dropdownOptions.map((option) => ( + + {option.label} + + ))} + + + ) } diff --git a/packages/ui/src/tabs.tsx b/packages/ui/src/tabs.tsx index 41e3cdba12..956746aa7e 100644 --- a/packages/ui/src/tabs.tsx +++ b/packages/ui/src/tabs.tsx @@ -5,51 +5,50 @@ import * as TabsPrimitive from "@radix-ui/react-tabs" import { cn } from "utils" -const Tabs = TabsPrimitive.Root +function Tabs({ className, ...props }: React.ComponentProps) { + return ( + + ) +} -const TabsList = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -TabsList.displayName = TabsPrimitive.List.displayName +function TabsList({ className, ...props }: React.ComponentProps) { + return ( + + ) +} -const TabsTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -TabsTrigger.displayName = TabsPrimitive.Trigger.displayName +function TabsTrigger({ className, ...props }: React.ComponentProps) { + return ( + + ) +} -const TabsContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -TabsContent.displayName = TabsPrimitive.Content.displayName +function TabsContent({ className, ...props }: React.ComponentProps) { + return ( + + ) +} export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/packages/ui/tailwind.config.cjs b/packages/ui/tailwind.config.cjs index f0962e1496..4eac1c7675 100644 --- a/packages/ui/tailwind.config.cjs +++ b/packages/ui/tailwind.config.cjs @@ -66,10 +66,28 @@ module.exports = { height: "0", }, }, + "collapsible-down": { + from: { + height: "0", + }, + to: { + height: "var(--radix-collapsible-content-height)", + }, + }, + "collapsible-up": { + from: { + height: "var(--radix-collapsible-content-height)", + }, + to: { + height: "0", + }, + }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", + "collapsible-down": "collapsible-down 0.2s ease-out", + "collapsible-up": "collapsible-up 0.2s ease-out", }, }, },