From dbf519fd4581dcb1f30b78035f3f069a49869810 Mon Sep 17 00:00:00 2001 From: Dan Farrelly Date: Thu, 19 Dec 2024 10:40:00 -0500 Subject: [PATCH] Docs: refreshed concurrency docs, callout webhooks api (#1031) * Update concurrency docs for clarity * Callout the webhooks api * Disable broken links for now until site is fixed --- app/globals.css | 4 +- pages/blog/[slug].tsx | 2 +- pages/blog/_posts/sharding-at-inngest.mdx | 2 +- pages/docs/guides/concurrency.mdx | 376 ++++++++++++---------- pages/docs/platform/webhooks.mdx | 25 +- shared/Docs/mdx.tsx | 52 ++- styles/globals.css | 4 +- 7 files changed, 289 insertions(+), 176 deletions(-) diff --git a/app/globals.css b/app/globals.css index 30291c366..332a5d781 100644 --- a/app/globals.css +++ b/app/globals.css @@ -143,7 +143,7 @@ body { --color-border-success: var(--color-matcha-700); --color-border-error: var(--color-ruby-600); --color-border-warning: var(--color-honey-600); - --color-border-info: var(--color-purplehaze-700); + --color-border-info: var(--color-breeze-600); --color-primary-2xSubtle: var(--color-matcha-200); --color-primary-xSubtle: var(--color-matcha-300); @@ -247,7 +247,7 @@ body { --color-border-success: var(--color-matcha-500); --color-border-error: var(--color-ruby-300); --color-border-warning: var(--color-honey-300); - --color-border-info: var(--color-purplehaze-300); + --color-border-info: var(--color-breeze-300); --color-primary-2xSubtle: var(--color-matcha-800); --color-primary-xSubtle: var(--color-matcha-700); diff --git a/pages/blog/[slug].tsx b/pages/blog/[slug].tsx index 058ca9586..1f8de4899 100644 --- a/pages/blog/[slug].tsx +++ b/pages/blog/[slug].tsx @@ -107,7 +107,7 @@ const authorURLs = { "Taylor Facen": "https://twitter.com/ItsTayFay", "Igor Samokhovets": "https://twitter.com/IgorSamokhovets", "Dave Kiss": "https://twitter.com/davekiss", - "Bruno Scheufler": "https://brunoscheufler.com", + // "Bruno Scheufler": "https://brunoscheufler.com", // removed while site is disabled "Lydia Hallie": "https://x.com/lydiahallie", "Joe Adams": "https://www.linkedin.com/in/josephadams9/", "Charly Poly": "https://x.com/whereischarly", diff --git a/pages/blog/_posts/sharding-at-inngest.mdx b/pages/blog/_posts/sharding-at-inngest.mdx index 250efe596..b58b9db20 100644 --- a/pages/blog/_posts/sharding-at-inngest.mdx +++ b/pages/blog/_posts/sharding-at-inngest.mdx @@ -32,7 +32,7 @@ Our first phase was to create a new interface layer that would coordinate how ou Two weeks after starting at Inngest, I joined forces with Jack Williams, our founding engineer, and kicked off work on a coordinator service mediating access to the state store. The state store persists events, step outputs, and other metadata on every function run. As users can return blobs and other data from their function run steps, we need to ensure incredibly fast reads and writes to the state store to keep latency as low as possible. -Adding this service boundary would enable connection pooling, centralized instrumentation for observability, and a single cut-over point to roll out changes across different user segments. Long-term, this also allows us to replace the Redis-based storage backend with a more scalable system tailored to our access patterns. If you're interested in our work on the state coordinator, please check out [my recent post](https://brunoscheufler.com/blog/2024-07-04-enhancing-scalability-and-reducing-latency-without-missing-a-beat) on the design decisions and rollout strategy we chose. +Adding this service boundary would enable connection pooling, centralized instrumentation for observability, and a single cut-over point to roll out changes across different user segments. Long-term, this also allows us to replace the Redis-based storage backend with a more scalable system tailored to our access patterns. {/* (NOTE Hidden until Bruno's site is back online) If you're interested in our work on the state coordinator, please check out [my recent post](https://brunoscheufler.com/blog/2024-07-04-enhancing-scalability-and-reducing-latency-without-missing-a-beat) on the design decisions and rollout strategy we chose. */} ## Sharding Redis diff --git a/pages/docs/guides/concurrency.mdx b/pages/docs/guides/concurrency.mdx index d69e9dd08..4094f559f 100644 --- a/pages/docs/guides/concurrency.mdx +++ b/pages/docs/guides/concurrency.mdx @@ -1,36 +1,218 @@ -import { Callout, CodeGroup, Properties, Property } from "src/shared/Docs/mdx"; +import { Callout, Info, Tip, Warning, CodeGroup, Properties, Property } from "src/shared/Docs/mdx"; # Concurrency management -Managing concurrency is important for any production system. Inngest allows you to manage concurrency limits in functions. Concurrency controls the number of steps executing code at any one time. It works by creating multi-level virtual queues within each function, directly in code, without thinking about infrastructure. +Limiting concurrency in systems is an important tool for correctly managing computing resources and scaling workloads. Inngest's concurrency control enables you to manage the number _steps_ that concurrently execute. -## Concurrency use cases +{/* TODO - Link to updated keys section */} +Step concurrency can be optionally configured using "keys" which applies the limit to each unique value of the key (ex. user id). The concurrency option can also be applied to different "scopes" which allows a concurrency limit to be shared across _multiple_ functions. -Use cases include: -- **Setting individual function concurrency limits**, for example to only run 10 imports at once. -- **Setting global limits for many functions which share a pool of resources**, for example with many functions which use shared AI capacity, or global DB connections. -- **Setting limits on your own individual accounts, tenants or users**, for example limiting unpaid users to a specific capacity. -- **A combination of the above**, with functions limited by the first concurrency key to hit limits. +As compared to traditional queue and worker systems, Inngest manages the concurrency within the system you do not need to implement additional worker-level logic or state. -## Examples and configuration +## When to use concurrency -Inngest lets you provide fine-grained concurrency across all functions in a simple, configurable manner. You can control each function's concurrency limits within your function definition. Here are two examples: +Concurrency is most useful when you want to constrain your function for a set of resources. Some use cases include: - +- **Limiting in multi-tenant systems** - Prevent a single account, user, or tenant from consuming too many resources and creating a backlog for others. See: [Concurrency keys (Multi-tenant concurrency)](#concurrency-keys-multi-tenant-concurrency). +- **Limiting throughput for database operations** - Prevent potentially high volume jobs from overwhelming a database or similar resource. See: [Sharing limits across functions (scope)](#sharing-limits-across-functions-scope). +- **Basic concurrent operations limits** - Limit the capacity dedicated to processing a certain job, for example an import pipeline. See: [Basic concurrency](#basic-concurrency). +- **Combining multiple of the above** - Multiple concurrency limits can be added per function. See: [Combining multiple concurrency limits](#combining-multiple-concurrency-limits) -```ts {{ title: "TypeScript" }} -// Example 1: a simple concurrency definition limiting this function to 10 steps at once. + +If you need to limit a function to a certain rate of processing, for example with a third party API rate limit, you might need [throttling](/docs/guides/throttling) instead. Throttling is applied at the function level, compared to concurrency which is at the step level. + + +## How to configure concurrency + +One or more concurrency limits can be configured for each function. + +* [Basic concurrency](#basic-concurrency) +* [Concurrency keys (Multi-tenant concurrency)](#concurrency-keys-multi-tenant-concurrency) +* [Sharing limits across functions (scope)](#sharing-limits-across-functions-scope) +* [Combining multiple concurrency limits](#combining-multiple-concurrency-limits) + +### Basic concurrency + +The most basic concurrency limit is a single `limit` set to an integer value of the maximum number of concurrently executing steps. When concurrency limit is reached, new steps will continue to be queued and create a backlog to be processed. + + +```ts inngest.createFunction( { - id: "another-function", + id: "generate-ai-summary", concurrency: 10, }, { event: "ai/summary.requested" }, async ({ event, step }) => { + // Your function handler here } ); +``` +```go +inngest.CreateFunction( + &inngestgo.FunctionOpts{ + Name: "generate-ai-summary", + Concurrency: []inngest.Concurrency{ + { + Limit: 10, + } + }, + }, + inngestgo.EventTrigger("ai/summary.requested", nil), + func(ctx context.Context, input inngestgo.Input) (any, error) { + // Your function handler here + return nil, nil + }, +) +``` +```python +@inngest.create_function( + fn_id="generate-ai-summary", + concurrency=[ + inngest.Concurrency( + limit=10, + ) + ] +) +async def first_function(event, step): + # Your function handler here + pass +``` + -// Example 2: A complete, complex example with two virtual concurrency queues. +### Concurrency keys (Multi-tenant concurrency) + +Use a concurrency `key` expression to apply the `limit` to each unique value of key received. Within the Inngest system, this creates a **virtual queue** for every unique value and limits concurrency to each. + + +```ts +inngest.createFunction( + { + id: "generate-ai-summary", + concurrency: [ + { + key: "event.data.account_id", + limit: 10, + }, + ], + }, + { event: "ai/summary.requested" }, + async ({ event, step }) => { + } +); +``` +```go +inngest.CreateFunction( + &inngestgo.FunctionOpts{ + Name: "generate-ai-summary", + Concurrency: []inngest.Concurrency{ + { + Scope: "fn", + Key: "event.data.account_id", + Limit: 10, + } + }, + }, + inngestgo.EventTrigger("ai/summary.requested", nil), + func(ctx context.Context, input inngestgo.Input) (any, error) { + // Your function handler here + return nil, nil + }, +) +``` +```python +@inngest.create_function( + fn_id="another-function", + concurrency=[ + inngest.Concurrency( + scope="fn", + key="event.data.account_id", + limit=10, + ) + ] +) +async def first_function(event, step): + # Your function handler here + pass +``` + + + + Concurrency keys are great for creating fair, multi-tenant systems. This can help prevent the noisy neighbor issue where one user triggers a lot of jobs and consumes far more resources that slow down your other users. + + +### Sharing limits across functions (scope) + +Using the `scope` option, limits can be set across your entire Inngest account, shared across multiple functions. Here is an example of setting an `"account"` level limit for a _static_ `key` equal to `"openai"`. This will create a virtual queue using `"openai"` as the key. Any other functions using this same `"openai"` key will consume from this same limit. + +{/* TODO - Link to the detail section on how this works */} + + +```ts +inngest.createFunction( + { + id: "generate-ai-summary", + concurrency: [ + { + scope: "account", + key: `"openai"`, + limit: 60, + }, + ], + }, + { event: "ai/summary.requested" }, + async ({ event, step }) => { + } +); +``` +```go +inngest.CreateFunction( + &inngestgo.FunctionOpts{ + Name: "generate-ai-summary", + Concurrency: []inngest.Concurrency{ + { + Scope: "account", + Key: `"openai"`, + Limit: 60, + } + }, + }, + inngestgo.EventTrigger("ai/summary.requested", nil), + func(ctx context.Context, input inngestgo.Input) (any, error) { + // Your function handler here + return nil, nil + }, +) +``` +```python +@inngest.create_function( + fn_id="another-function", + concurrency=[ + inngest.Concurrency( + scope="account", + key='"openai"', + limit=60, + ) + ] +) +async def first_function(event, step): + # Your function handler here + pass +``` + + +### Combining multiple concurrency limits + +Each SDK's concurrency option supports up to two limits. This is the most beneficial when combining limits, each with a different `scope`. Here is an example that combines two limits, one on the `"account"` scope and another on the `"fn"` level. Combining limits will create multiple virtual queues to limit concurrency. In the below function: + +- If there are 10 steps executing under the 'openai' key's virtual queue, any future runs will be blocked and will wait for existing runs to finish before executing. +- If there are 5 steps executing under the 'openai' key and a single `event.data.account_id` enqueues 2 runs, the second run is limited by the `event.data.account_id` virtual queue and will wait before executing. + + + + + +```ts {{ title: "TypeScript" }} inngest.createFunction( { id: "unique-function-id", @@ -41,15 +223,13 @@ inngest.createFunction( // runs using the same "openai"` key counts towards this limit. scope: "account", key: `"openai"`, - // If there are 10 functions running with the "openai" key, this function's - // runs will wait for capacity before executing. limit: 10, }, { // Create another virtual concurrency queue for this function only. This // limits all accounts to a single execution for this function, based off // of the `event.data.account_id` field. - // "fn" is the default scope, so we could omit this field. + // NOTE - "fn" is the default scope, so we could omit this field. scope: "fn", key: "event.data.account_id", limit: 1, @@ -60,28 +240,9 @@ inngest.createFunction( async ({ event, step }) => { } ); - ``` ```go {{ title: "Go" }} -// Example 1: a simple concurrency definition limiting this function to 10 steps at once. -inngest.CreateFunction( - &inngestgo.FunctionOpts{ - Name: "another-function", - Concurrency: []inngest.Concurrency{ - { - Limit: 10, - } - }, - }, - inngestgo.EventTrigger("ai/summary.requested", nil), - func(ctx context.Context, input inngestgo.Input) (any, error) { - // Function implementation here - return nil, nil - }, -) - -// Example 2: A complete, complex example with two virtual concurrency queues. inngestgo.CreateFunction( &inngestgo.FunctionOpts{ Name: "unique-function-id", @@ -92,15 +253,13 @@ inngestgo.CreateFunction( // runs using the same "openai" key counts towards this limit. Scope: "account", Key: `"openai"`, - // If there are 10 functions running with the "openai" key, this function's - // runs will wait for capacity before executing. Limit: 10, }, { // Create another virtual concurrency queue for this function only. This // limits all accounts to a single execution for this function, based off // of the `event.data.account_id` field. - // "fn" is the default scope, so we could omit this field. + // NOTE - "fn" is the default scope, so we could omit this field. Scope: "fn", Key: "event.data.account_id", Limit: 1, @@ -115,23 +274,7 @@ inngestgo.CreateFunction( ) ``` - - ```py {{ title: "Python" }} -# Example 1: a simple concurrency definition limiting this function to 10 steps at once. -@inngest.create_function( - fn_id="another-function", - concurrency=[ - inngest.Concurrency( - limit=10, - ) - ] -) -async def first_function(event, step): - # Function implementation here - pass - -# Example 2: A complete, complex example with two virtual concurrency queues. @inngest.create_function( fn_id="unique-function-id", concurrency=[ @@ -141,15 +284,13 @@ async def first_function(event, step): # runs using the same "openai" key counts towards this limit. scope="account", key='"openai"', - # If there are 10 functions running with the "openai" key, this function's - # runs will wait for capacity before executing. limit=10, ), inngest.Concurrency( # Create another virtual concurrency queue for this function only. This # limits all accounts to a single execution for this function, based off # of the `event.data.account_id` field. - # "fn" is the default scope, so we could omit this field. + # NOTE - "fn" is the default scope, so we could omit this field. scope="fn", key="event.data.account_id", limit=1, @@ -160,23 +301,21 @@ async def handle_ai_summary(event, step): # Function implementation here pass ``` - - -In the first example, the function is constrained to `10` executing steps at once. - -In the second example, we define **two concurrency constraints that create two virtual queues to manage capacity**. Runs will be limited if they hit either of the virtual queue's limits. For example: -- If there are 10 steps executing under the 'openai' key's virtual queue, any future runs will be blocked and will wait for existing runs to finish before executing. -- If there are 5 steps executing under the 'openai' key and a single `event.data.account_id` enqueues 2 runs, the second run is limited by the `event.data.account_id` virtual queue and will wait before executing. + + It's worth it to note that the `"fn"` scope is the default and is optional to include. + ## How concurrency works **Concurrency works by limiting the number of steps executing at a single time.** Within Inngest, execution is defined as "an SDK running code". **Calling **`step.sleep`**, **`step.sleepUntil`**, **`step.waitForEvent`**, or **`step.invoke`** does not count towards capacity limits**, as the SDK doesn't execute code while those steps wait. Because sleeping or waiting is common, concurrency _does not_ limit the number of functions in progress. Instead, it limits the number of steps executing at any single time. -**Queues are ordered from oldest to newest jobs ([FIFO](https://en.wikipedia.org/wiki/FIFO))** across the same function. Ordering amongst different functions is not guaranteed. This means that within a specific function, Inngest prioritizes finishing older functions above starting newer functions - even if the older functions continue to schedule new steps to run. Different functions, however, compete for capacity, with runs on the most backlogged function much more likely (but not guaranteed) to be scheduled first. + + Steps that are asynchronous actions, `step.sleep`, `step.sleepUntil`, `step.waitForEvent`, and `step.invoke` do not contribute to the concurrency limit. + -Inngest manages concurrency for you within our scheduling system, and you do not need to provision queues, infrastructure, or manage concurrency limits within your own workers or services. +**Queues are ordered from oldest to newest jobs ([FIFO](https://en.wikipedia.org/wiki/FIFO))** across the same function. Ordering amongst different functions is not guaranteed. This means that within a specific function, Inngest prioritizes finishing older functions above starting newer functions - even if the older functions continue to schedule new steps to run. Different functions, however, compete for capacity, with runs on the most backlogged function much more likely (but not guaranteed) to be scheduled first. Some additional information: @@ -285,7 +424,7 @@ async def func_a(ctx: inngest.Context, step: inngest.Step): pass @inngest_client.create_function( - fn_id="func-b", + fn_id="func-b", trigger=inngest.TriggerEvent(event="ai/summary.requested"), concurrency=[ inngest.Concurrency( @@ -311,7 +450,7 @@ Because functions are FIFO, function runs are more likely to be worked on the ol - Concurrency limits the number of steps executing at a single time. It does not _yet_ perform rate limiting over a given period of time. - Functions can specify up to 2 concurrency constraints at once -- The maximum concurrency limit is defined by your account's plan +- The maximum concurrency limit is defined by your account's plan - Ordering amongst the same function is guaranteed (with the exception of retries) - Ordering amongst different functions is not guaranteed. Functions compete with each other randomly to be scheduled. @@ -321,7 +460,7 @@ Because functions are FIFO, function runs are more likely to be worked on the ol The maximum number of concurrently running steps. A value of `0` or `undefined` is the equivalent of not setting a limit. - The maximum value is dictated by your account's plan. + The maximum value is dictated by your account's plan. The scope for the concurrency limit, which impacts whether concurrency is managed on an individual function, across an environment, or across your entire account. @@ -345,95 +484,6 @@ Because functions are FIFO, function runs are more likely to be worked on the ol ## Further examples -### Handling third party API rate limits - -Here, we use the Resend SDK to send an email. Resend's rate limit is 10 requests per second so we set a lower concurrency as our function is simple and may execute multiple times per second. Here we use a limit of `4` to keep the throughput a bit slower than necessary: - - - -```ts {{ title: "TypeScript" }} -export const send = inngest.createFunction( - { - name: "Email: Pending invoice", - id: "email-pending-invoice", - concurrency: { - limit: 4, // Resend's rate limit is 10 req/s - }, - }, - { event: "billing/invoice.pending" }, - async ({ event, step }) => { - await step.run("send-email", async () => { - return await resend.emails.send({ - from: "hello@myco.com", - to: event.user.email, - subject: `Invoice pending for ${event.data.invoicePeriod}`, - text: `Dear user, ...`, - }); - }); - - return { message: "success" }; - } -); -``` - - -```go {{ title: "Go" }} -inngest.CreateFunction( - &inngestgo.FunctionOpts{ - Name: "Email: Pending invoice", - ID: "email-pending-invoice", - Concurrency: []inngest.Concurrency{ - { - Limit: 4, // Resend's rate limit is 10 req/s - }, - }, - }, - inngestgo.EventTrigger("billing/invoice.pending", nil), - func(ctx context.Context, input inngestgo.Input) (any, error) { - _, err := input.Step.Run(ctx, "send-email", func(ctx context.Context) (any, error) { - return resend.Emails.Send(&resend.SendEmailRequest{ - From: "hello@myco.com", - To: input.Event.User.Email, - Subject: fmt.Sprintf("Invoice pending for %s", input.Event.Data.InvoicePeriod), - Text: "Dear user, ...", - }) - }) - if err != nil { - return nil, err - } - - return map[string]string{"message": "success"}, nil - }, -) -``` - - - -```python {{ title: "Python" }} -@inngest.create_function( - fn_id="email-pending-invoice", - name="Email: Pending invoice", - concurrency=[ - inngest.Concurrency( - limit=4, # Resend's rate limit is 10 req/s - ) - ] -) -async def send(event, step): - async with step.run("send-email") as _: - params: resend.Emails.SendParams = { - "from": "Acme ", - "to": ["delivered@resend.dev"], - "subject": "hello world", - "html": "it works!", - } - await resend.emails.send(params) - - return {"message": "success"} -``` - - - ### Restricting parallel import jobs for a customer id In this hypothetical system, customers can upload `.csv` files which each need to be processed and imported. We want to limit each customer to only one import job at a time so no two jobs are writing to a customer's data at a given time. We do this by setting a `limit: 1` and a concurrency `key` to the `customerId` which is included in every single event payload. @@ -442,7 +492,7 @@ Inngest ensures that the concurrency (`1`) applies to each unique value for `eve -```ts {{ title: "TypeScript" }} +```ts {{ title: "TypeScript" }} export const send = inngest.createFunction( { name: "Process customer csv import", @@ -511,7 +561,7 @@ async def process_csv_import(ctx: inngest.Context, step: inngest.Step): async def process_file(): file = await bucket.fetch(ctx.event.data.file_uri) # ... - + await step.run("process-file", process_file) return {"message": "success"} ``` @@ -520,4 +570,4 @@ async def process_csv_import(ctx: inngest.Context, step: inngest.Step): ## Tips -* Configure [start timeouts](/docs/features/inngest-functions/cancellation/cancel-on-timeouts) to prevent large backlogs with concurrency +* Configure [start timeouts](/docs/features/inngest-functions/cancellation/cancel-on-timeouts) to prevent large backlogs with concurrency diff --git a/pages/docs/platform/webhooks.mdx b/pages/docs/platform/webhooks.mdx index e02738097..8073b7856 100644 --- a/pages/docs/platform/webhooks.mdx +++ b/pages/docs/platform/webhooks.mdx @@ -1,4 +1,8 @@ -import { Callout, Row, Col, Properties, Property } from "src/shared/Docs/mdx"; +import { Callout, Row, Col, Properties, Property, CardGroup, Card } from "src/shared/Docs/mdx"; +import { + RiTerminalLine, + RiGithubFill +} from "@remixicon/react"; # Consuming webhook events @@ -209,6 +213,25 @@ Additionally, you can configure allow/deny lists for event names and IP addresse Inngest currently does not yet support webhook signature verification. While this means that you cannot use the signature to verify the authenticity of the payload, each webhook URL is unique and can only be used by one producer. This means that you can be confident that the events are coming from the producer that you expect. +## Managing webhooks via REST API + +Webhooks can be created, updated and deleted all via the Inngest REST API. This is very useful if you want to manage all transforms within your codebase and sync them to the Inngest platform. Check out the documentation below to learn more: + + + } + href={'https://api-docs.inngest.com/docs/inngest-api/b539bae406d1f-get-all-webhook-endpoints-in-given-environment'}> + Read the documentation about managing Webhooks via the Inngest REST API + + } + href={'https://github.com/inngest/webhook-transform-sync'}> + View an end-to-end example of how to test and sync Webhooks in your codebase. + + + ## Local development To test your webhook locally, you can forward events to the [Dev Server](/docs/local-development) from the Inngest dashboard using the "Send to Dev Server" button. This button is found anywhere that an event payload is visible on the Inngest dashboard. This will send a copy of the event to your local machine where you can test your functions. diff --git a/shared/Docs/mdx.tsx b/shared/Docs/mdx.tsx index 612a5cb11..7146ef391 100644 --- a/shared/Docs/mdx.tsx +++ b/shared/Docs/mdx.tsx @@ -6,6 +6,12 @@ import { Heading } from "./Heading"; import React, { useState } from "react"; import { ChevronDown, ChevronUp } from "react-feather"; import Image, { ImageProps } from "next/image"; +import { + RiLightbulbLine, + RiInformationLine, + RiAlertLine, + type RemixiconComponentType, +} from "@remixicon/react"; import Zoom from "react-medium-image-zoom"; import "react-medium-image-zoom/dist/styles.css"; @@ -119,25 +125,35 @@ export function Callout({ Icon = null, children, }: { - variant: "default" | "info" | "warning"; - Icon?: typeof React.Component; + variant: "default" | "info" | "warning" | "tip"; + Icon?: typeof React.Component | RemixiconComponentType; children: React.ReactNode; }) { return (
:first-child]:mt-0 [&>:last-child]:mb-0", + // Setting the dark variants for text are necessary to override other selectors (variant === "default" || variant === "info") && - "dark:border-sky-600/20 text-sky-600 dark:text-sky-100 bg-sky-300/10", + "text-info dark:text-info bg-info dark:bg-info/50", variant === "warning" && - "dark:border-amber-700/20 text-amber-900 dark:text-amber-50 bg-amber-300/10", + "text-warning dark:text-warning bg-warning dark:bg-warning/50", + variant === "tip" && + "text-success dark:text-success bg-success dark:bg-success/50", Icon && "flex gap-2.5" )} > {Icon ? ( <> - +
{children}
@@ -149,6 +165,30 @@ export function Callout({ ); } +export function Info({ children }) { + return ( + + {children} + + ); +} + +export function Tip({ children }) { + return ( + + {children} + + ); +} + +export function Warning({ children }) { + return ( + + {children} + + ); +} + export function ButtonCol({ children }) { return (
diff --git a/styles/globals.css b/styles/globals.css index 96356a4fb..338d6eb2b 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -944,7 +944,7 @@ ul.check li { --color-border-success: var(--color-matcha-700); --color-border-error: var(--color-ruby-600); --color-border-warning: var(--color-honey-600); - --color-border-info: var(--color-purplehaze-700); + --color-border-info: var(--color-breeze-600); --color-primary-2xSubtle: var(--color-matcha-200); --color-primary-xSubtle: var(--color-matcha-300); @@ -1048,7 +1048,7 @@ ul.check li { --color-border-success: var(--color-matcha-500); --color-border-error: var(--color-ruby-300); --color-border-warning: var(--color-honey-300); - --color-border-info: var(--color-purplehaze-300); + --color-border-info: var(--color-breeze-300); --color-primary-2xSubtle: var(--color-matcha-800); --color-primary-xSubtle: var(--color-matcha-700);