Merge remote-tracking branch 'origin/main' into glm/quick-datatable-onboarding

# Conflicts:
#	frontend/src/lib/components/AppConnectInner.svelte
#	frontend/src/lib/components/icons/SupabaseIcon.svelte
This commit is contained in:
Guilhem Lemouel
2026-08-19 09:13:29 +02:00
448 changed files with 11371 additions and 2896 deletions
+220 -88
View File
@@ -1,17 +1,31 @@
#!/usr/bin/env bash
# PreToolUse allowance for scratch file ops: auto-allow a single, plain, single-line
# `mkdir` / `cp` / `mv` / `touch` / `chmod` / `tar` / `unzip` whose every path operand
# resolves under /tmp. Anything else makes no decision (exit 0) and falls back to the normal
# permission flow, except for `mv` and `chmod`: those get an explicit `ask`, the only prompt
# they get (see lib-guarded-verb.sh).
# PreToolUse allowance for scratch file ops: auto-allow `mkdir` / `cp` / `mv` / `touch` /
# `chmod` whose every path operand resolves inside one of the roots `path_class` recognizes —
# under /tmp, or inside a git working tree under $HOME — and `tar` / `unzip` confined to /tmp.
# Anything else makes no decision (exit 0) and falls back to the normal permission flow, except
# for `mv` and `chmod`: those get an explicit `ask`, the only prompt they get (see
# lib-guarded-verb.sh).
#
# The command is read one segment at a time, so chaining and line breaks carry no weight of
# their own: `cd /tmp/scratch && mv /tmp/a /tmp/b` is proved on the operands of the `mv`. A
# decision covers the whole command line, so `allow` is emitted only when every segment is one
# of these verbs proved here or a `cd` that resolved, AND exactly one of them writes (see the
# gate at the foot of this file — an earlier write can change what a later operand means). A
# line that mixes a proven op with some other command makes no decision instead and leaves that
# line to the normal permission flow, rather than waving an unexamined command through with it.
#
# This is a hook rather than an allow rule because permission rules match a command prefix, so
# they can only constrain the FIRST operand. `cp /tmp/x ~/.zshrc` matches a `cp /tmp/` prefix,
# and requiring every operand is the point.
#
# Requiring the sources under /tmp too (not just the destination) keeps this from becoming a
# read-exfiltration path around the `Read(**/.env)` / `Read(**/secrets/**)` deny rules: a copy
# out of the project into /tmp would land the content somewhere `Read(/tmp/**)` allows.
# One operation may not straddle two roots, sources included, and a sibling checkout is a
# different root — `path_class` names the git tree, not just its kind. A copy out of a checkout
# into /tmp would be a read-exfiltration path around the `Read(**/secrets/**)` / `Read(**/*.pem)`
# deny rules, since the content lands where `Read(/tmp/**)` allows it to be read back, and one
# out of a repo the Read tool is not confined to would do the same for that repo. Keeping every
# operand of one operation inside a single root closes both without restating those rules here.
# The checkout root itself is what makes an in-repo `mv` or `chmod` auto-allowable: deleting a
# file there has never prompted, and moving or chmod-ing one is not the graver act.
#
# Deny-by-default tokenizing, in the same spirit as guard-rm-outside-tmp.sh: every path token
# must consist only of alphanumerics and `. _ / -`. That set contains none of the characters
@@ -19,12 +33,19 @@
# any glob character, so all of those forms fail by construction. `realpath -m` then resolves
# `..` and existing symlinks, so `/tmp/link` pointing at /etc/passwd is caught.
#
# `tar` and `unzip` keep the stricter rule — /tmp only, and absolute operands only — because
# their positional grammar makes a bare word ambiguous: `tar P -xf ...` is --absolute-names,
# not a file named P, and resolving it as a path would put an option in a root and allow it.
# The other five take relative operands, resolved against the working directory that `cd`
# tracking maintains, since for those a bare word really is a path (a GNU option starts with
# `-`, and the option allowlist below rejects the ones that would change symlink handling).
#
# `tar` and `unzip` get their own parser: their write destination arrives as a flag VALUE
# (`-C`, `-d`) rather than a positional, and a bundle like `-xzf` consumes the token after it.
# Flags are an allowlist, not a denylist, so `-P` / `--absolute-names` — which turn off tar's
# refusal to extract `..` and absolute member paths — defer rather than needing enumeration.
# Extraction additionally requires an explicit destination under /tmp, or a cwd already under
# /tmp, since otherwise members land in the project checkout.
# Extraction additionally requires an explicit destination under /tmp, or a working directory
# already under /tmp, since otherwise members land in the project checkout.
#
# Residual risk accepted: an archive whose members include a symlink pointing out of /tmp
# followed by a write through it can still escape, because tar applies member symlinks as it
@@ -52,25 +73,51 @@ defer() {
exit 0
}
# A newline separates commands, and the tokenizer below only reads the first line — defer.
case "$cmd" in *$'\n'*) defer "multi-line command" ;; esac
has_substitution "$cmd" && defer "command substitution in the command line"
read -r -a toks <<< "$cmd"
# 0 iff the token is a literal path this hook may reason about. A glob never auto-allows: bash
# expands it only after the hook has decided, so realpath sees the unexpanded pattern —
# `/tmp/link*` canonicalizes to itself and passes, then expands onto a symlink whose target is
# outside, and `cp` and `chmod` follow a command-line symlink, so that is a write to the target.
# (guard-rm-outside-tmp.sh can allow globs because `rm` unlinks the symlink rather than following
# it.) The charset holds none of the characters bash uses for quoting, expansion or separation.
literal_path() {
case "$1" in *[*?[]*) return 1 ;; esac
[ -z "$(printf '%s' "$1" | tr -d 'A-Za-z0-9._/-')" ]
}
# 0 iff the token is charset-safe and resolves to a path strictly inside /tmp.
# Prints the root class of a path token, then the path it resolved to on a second line,
# resolving a relative one against the tracked working directory. Fails, printing nothing,
# when the token is unsafe to reason about or lands outside every root.
operand_class() {
local t="$1" canon alt cls alt_cls=""
literal_path "$t" || return 1
case "$t" in
/*) canon=$(realpath -m -- "$t" 2>/dev/null) ;;
*) # A `cd` may fail at runtime and leave the command where it started, so a relative
# operand has to land in the same root either way.
[ -n "$seg_cwd" ] || return 1
canon=$(realpath -m -- "$seg_cwd/$t" 2>/dev/null)
if [ -n "$alt_cwd" ]; then
alt=$(realpath -m -- "$alt_cwd/$t" 2>/dev/null)
[ -n "$alt" ] || return 1
alt_cls=$(path_class "$alt") || return 1
fi
;;
esac
[ -n "$canon" ] || return 1
cls=$(path_class "$canon") || return 1
[ -n "$alt_cls" ] && [ "$alt_cls" != "$cls" ] && return 1
# Class and resolved path together: a caller runs this in a command substitution, so a global
# set here would be set in that subshell and lost.
printf '%s\n%s' "$cls" "$canon"
}
# 0 iff the token is charset-safe and resolves to a path strictly inside /tmp. The archive
# parser's stricter check; everything else goes through operand_class.
under_tmp() {
local t="$1" canon
# Globs never auto-allow. Bash expands them only after this hook has decided, so realpath
# sees the unexpanded pattern: `/tmp/link*` canonicalizes to itself and passes, then
# expands onto a symlink whose target is outside /tmp. chmod and cp follow command-line
# symlinks, so that is a write to the target. guard-rm-outside-tmp.sh can allow globs
# because `rm` unlinks the symlink itself rather than following it.
case "$t" in *[*?[]*) return 1 ;; esac
[ -n "$(printf '%s' "$t" | tr -d 'A-Za-z0-9._/-')" ] && return 1
# Absolute only. Resolving a relative operand against the cwd makes any bare word look like
# a safe path whenever the cwd is under /tmp, while the tool itself reads it as an option:
# `tar P -xf ...` is --absolute-names, not ./P, and `cp /tmp/t -RL /tmp/o` is a
# dereferencing recursive copy, not a file named -RL.
literal_path "$t" || return 1
case "$t" in /*) ;; *) return 1 ;; esac
canon=$(realpath -m -- "$t" 2>/dev/null)
[ -n "$canon" ] || return 1
@@ -79,29 +126,16 @@ under_tmp() {
return 1
}
# Bare command word only; wrappers (`timeout cp`), env prefixes, and `/bin/cp` defer.
# Options are an allowlist per command, so anything that changes how symlinks are followed
# defers instead of needing enumeration. `cp -L` / `-H` matter most: they dereference while
# recursing, which copies the CONTENT of a symlink target from outside /tmp into a scratch
# dir that `Read(/tmp/**)` then exposes. Plain `-r` and `-a` (which implies `-d`) recreate
# such a symlink as a symlink instead, so no outside content is materialized.
case "${toks[0]:-}" in
mkdir) takes_mode=0; ok_opts='pv' ;;
cp) takes_mode=0; ok_opts='rRvfnpa' ;;
mv) takes_mode=0; ok_opts='vfn' ;;
touch) takes_mode=0; ok_opts='acmv' ;;
chmod) takes_mode=1; ok_opts='Rvfc' ;; # chmod's first operand is a mode, not a path
tar) ok_flags='xctzjJavfC'; val_flags='fC' ;;
unzip) ok_flags='oqnljvd'; val_flags='d' ;;
*) defer "not the leading command word" ;;
esac
# ---------------------------------------------------------------- tar / unzip
if [ -n "${ok_flags:-}" ]; then
saw_archive=0 saw_dest=0 extracting=0 listing=0 end_opts=0
i=1
while [ "$i" -lt "${#toks[@]}" ]; do
t="${toks[$i]}"
# Proves one `tar` / `unzip` segment ($1 = the verb), whose tokens are in SEG_TOKS.
check_archive_segment() {
local verb="$1" ok_flags val_flags t flags val
local saw_archive=0 saw_dest=0 extracting=0 listing=0 end_opts=0 i=1
case "$verb" in
tar) ok_flags='xctzjJavfC'; val_flags='fC' ;;
unzip) ok_flags='oqnljvd'; val_flags='d' ;;
esac
while [ "$i" -lt "${#SEG_TOKS[@]}" ]; do
t="${SEG_TOKS[$i]}"
i=$((i + 1))
if [ "$end_opts" = 0 ]; then
[ "$t" = "--" ] && { end_opts=1; continue; }
@@ -112,13 +146,13 @@ if [ -n "${ok_flags:-}" ]; then
# leave a residue here and defer rather than being enumerated as denials.
[ -n "$(printf '%s' "$flags" | tr -d "$ok_flags")" ] && defer "unrecognized option \`$t\`"
case "$flags" in *x*) extracting=1 ;; esac
case "${toks[0]}$flags" in unzip*[lv]*) listing=1 ;; esac
case "$verb$flags" in unzip*[lv]*) listing=1 ;; esac
# A flag consuming the next token must be alone in its bundle's final position
# (`-xzf a.tar`), else the token it eats is ambiguous.
case "${flags%?}" in *[$val_flags]*) defer "ambiguous option bundle \`$t\`" ;; esac
case "${flags: -1}" in
[$val_flags])
val="${toks[$i]:-}"
val="${SEG_TOKS[$i]:-}"
i=$((i + 1))
[ -n "$val" ] || defer "option \`$t\` has no value"
under_tmp "$val" || defer "\`$val\` is outside /tmp"
@@ -136,54 +170,152 @@ if [ -n "${ok_flags:-}" ]; then
# first is the archive. Requiring every one under /tmp is conservative for member names,
# which are not filesystem paths — those defer rather than being wrongly allowed.
under_tmp "$t" || defer "\`$t\` is outside /tmp"
[ "${toks[0]}" = "unzip" ] && saw_archive=1
[ "$verb" = "unzip" ] && saw_archive=1
done
# tar without -f reads a tape/stdin; unzip needs an archive
[ "$saw_archive" = 1 ] || defer "no archive operand"
# Writes land relative to the working directory unless a destination was given. `unzip -l`
# and `-v` only list, so they need no destination.
if [ "$extracting" = 1 ] || { [ "${toks[0]}" = "unzip" ] && [ "$listing" = 0 ]; }; then
[ "$saw_dest" = 1 ] || under_tmp "${cwd:-$PWD}" || defer "extraction target is outside /tmp"
if [ "$extracting" = 1 ] || { [ "$verb" = "unzip" ] && [ "$listing" = 0 ]; }; then
# An extraction with no destination lands in the working directory. Word splitting cannot
# tell a `cd` inside a quoted string from one the shell runs, and believing a false one
# would put an archive's members in the checkout, so once any `cd` is in the line only an
# explicit destination will do.
[ "$saw_dest" = 1 ] \
|| { [ "$saw_cd" = 0 ] && [ -n "$seg_cwd" ] && under_tmp "$seg_cwd"; } \
|| defer "extraction target is outside /tmp"
fi
decide allow "archive paths and extraction target are under /tmp"
fi
}
# ------------------------------------------- mkdir / cp / mv / touch / chmod
path_operand=0
seen_mode=0
end_opts=0
i=1
while [ "$i" -lt "${#toks[@]}" ]; do
t="${toks[$i]}"
i=$((i + 1))
# Proves one `mkdir` / `cp` / `mv` / `touch` / `chmod` segment ($1 = the verb), whose tokens
# are in SEG_TOKS.
check_fileops_segment() {
local verb="$1" takes_mode ok_opts t cls resolved seen_class=""
local path_operand=0 seen_mode=0 end_opts=0 i=1 rel_operand=0
local -a ops=()
# Options are an allowlist per command, so anything that changes how symlinks are followed
# defers instead of needing enumeration. `cp -L` / `-H` matter most: they dereference while
# recursing, which copies the CONTENT of a symlink target from outside /tmp into a scratch
# dir that `Read(/tmp/**)` then exposes. Plain `-r` and `-a` (which implies `-d`) recreate
# such a symlink as a symlink instead, so no outside content is materialized.
case "$verb" in
mkdir) takes_mode=0; ok_opts='pv' ;;
cp) takes_mode=0; ok_opts='rRvfnpa' ;;
mv) takes_mode=0; ok_opts='vfn' ;;
touch) takes_mode=0; ok_opts='acmv' ;;
chmod) takes_mode=1; ok_opts='Rvfc' ;; # chmod's first operand is a mode, not a path
esac
while [ "$i" -lt "${#SEG_TOKS[@]}" ]; do
t="${SEG_TOKS[$i]}"
i=$((i + 1))
if [ "$end_opts" = 0 ]; then
[ "$t" = "--" ] && { end_opts=1; continue; }
# Checked at any position, not just before the first operand: GNU utils permute, so
# `cp /tmp/tree -RL /tmp/out` still enables dereferencing recursion.
case "$t" in
-?*)
# Allowlist: long options and the dereferencing flags leave a residue and defer.
[ -n "$(printf '%s' "${t#-}" | tr -d "$ok_opts")" ] && defer "unrecognized option \`$t\`"
continue
;;
esac
fi
if [ "$end_opts" = 0 ]; then
[ "$t" = "--" ] && { end_opts=1; continue; }
# Checked at any position, not just before the first operand: GNU utils permute, so
# `cp /tmp/tree -RL /tmp/out` still enables dereferencing recursion.
case "$t" in
-?*)
# Allowlist: long options and the dereferencing flags leave a residue and defer.
[ -n "$(printf '%s' "${t#-}" | tr -d "$ok_opts")" ] && defer "unrecognized option \`$t\`"
continue
;;
esac
fi
# chmod: consume the mode operand without a path check. Octal, or symbolic clauses.
if [ "$takes_mode" = 1 ] && [ "$seen_mode" = 0 ]; then
case "$t" in
[0-7] | [0-7][0-7] | [0-7][0-7][0-7] | [0-7][0-7][0-7][0-7]) ;;
*) printf '%s' "$t" | grep -Eq '^[ugoa]*[+=-][rwxXst]*(,[ugoa]*[+=-][rwxXst]*)*$' || defer "unrecognized mode \`$t\`" ;;
esac
seen_mode=1
continue
fi
# chmod: consume the mode operand without a path check. Octal, or symbolic clauses.
if [ "$takes_mode" = 1 ] && [ "$seen_mode" = 0 ]; then
case "$t" in
[0-7] | [0-7][0-7] | [0-7][0-7][0-7] | [0-7][0-7][0-7][0-7]) ;;
*) printf '%s' "$t" | grep -Eq '^[ugoa]*[+=-][rwxXst]*(,[ugoa]*[+=-][rwxXst]*)*$' || defer "unrecognized mode \`$t\`" ;;
esac
seen_mode=1
continue
fi
under_tmp "$t" || defer "\`$t\` is outside /tmp"
path_operand=1
resolved=$(operand_class "$t") || defer "\`$t\` is outside /tmp and not inside a git checkout in \$HOME"
cls="${resolved%%$'\n'*}"
# Every operand of one operation stays in one root: see the exfiltration note above.
[ -n "$seen_class" ] && [ "$cls" != "$seen_class" ] && defer "\`$t\` puts this $verb across two roots"
seen_class="$cls"
ops+=("${resolved#*$'\n'}")
case "$t" in /*) ;; *) rel_operand=1 ;; esac
path_operand=1
done
[ "$path_operand" = 1 ] || defer "no path operand"
# In directory form the command writes a path it does not name: `cp x dir` writes `dir/x`,
# and `cp` follows that child when it is a symlink — this checkout is full of them, every
# `*_ee.rs` pointing into the sibling EE repo. Deriving that child would mean reproducing
# which name the tool picks (the operand as written, not as resolved — a symlinked source
# keeps its own name) and how deep `-r` recurses. The form is left unproved instead.
case "$verb" in
cp | mv)
[ "${#ops[@]}" -ge 2 ] || return 0
# Whether the destination is an existing directory is itself a question about which of
# the two candidate working directories the command ran in, and only one of them is in
# `ops`. A `cd` that fails at runtime would otherwise let the form through: the
# destination resolved against the directory the command never reached is some path that
# does not exist, while the one it actually ran in is a directory full of symlinks.
[ -n "$alt_cwd" ] && [ "$rel_operand" = 1 ] \
&& defer "a relative operand after a \`cd\` lands in one of two directories"
[ -d "${ops[-1]}" ] \
&& defer "\`${ops[-1]}\` already exists as a directory, so this $verb writes a path it does not name"
;;
esac
}
split_segments "$cmd"
seg_cwd="${cwd:-$PWD}"
alt_cwd="" # where a `cd` that failed would have left the command
saw_cd=0 # a `cd` moved the working directory somewhere
proved=0 # how many ops came out inside a single root
only_ours=1 # ... and nothing else shares the command line
for seg in "${SEGMENTS[@]}"; do
segment_tokens "$seg"
case "${SEG_TOKS[0]:-}" in
"") continue ;;
mkdir | cp | mv | touch | chmod)
check_fileops_segment "${SEG_TOKS[0]}"
proved=$((proved + 1))
continue
;;
tar | unzip)
check_archive_segment "${SEG_TOKS[0]}"
proved=$((proved + 1))
continue
;;
cd)
# A `cd` writes nothing, so it never blocks an allow; it only moves where a later relative
# operand points, to one of the two candidates `apply_cd` describes.
if [ "$saw_cd" = 0 ] && new_cwd=$(apply_cd "$seg_cwd" "${SEG_TOKS[@]:1}"); then
alt_cwd="$seg_cwd"
seg_cwd="$new_cwd"
else
# Not the harmless segment an allow assumes: whatever this guard could not account for
# may be a redirect, and a redirect writes. Leave the line to the normal flow.
seg_cwd="" alt_cwd=""
only_ours=0
fi
saw_cd=1
continue
;;
esac
# Some other command shares the line. If an `mv` or `chmod` runs inside it after all — behind
# a wrapper, an env prefix or a path — this hook cannot say what it writes to.
for verb in mv chmod; do
segment_runs_verb "$verb" "$seg" && defer "$verb is not the leading command word in \`$seg\`"
done
only_ours=0
done
[ "$path_operand" = 1 ] || defer "no path operand"
decide allow "every path operand is under /tmp"
# Exactly one write per line. Each segment is proved against the filesystem as it stands now,
# and an earlier write can change what a later operand means: `cp -r /tmp/tree /tmp/live` that
# recreates a symlink out of /tmp turns `/tmp/live/link` — a path under /tmp when this ran —
# into a write through that symlink. Deletes compose safely and guard-rm-outside-tmp.sh allows
# several, because `rm` unlinks a symlink rather than following it.
[ "$proved" -ge 1 ] || exit 0
[ "$only_ours" = 1 ] && [ "$proved" = 1 ] && decide allow "every path operand is inside a single root"
exit 0
+104 -85
View File
@@ -1,14 +1,17 @@
#!/usr/bin/env bash
# PreToolUse guard for `rm`: auto-allow ONLY a single, plain, single-line `rm` whose every
# operand is a whitelisted target — under /tmp, or inside a git working tree located in $HOME
# (a version-controlled project dir). Any other command that runs `rm` gets an explicit `ask`,
# which is the ordinary permission prompt and the only one `rm` gets (see lib-guarded-verb.sh);
# a command that runs no `rm` at all makes no decision (exit 0).
# PreToolUse guard for `rm`: auto-allow deletes whose every operand is a whitelisted target —
# under /tmp, or inside a git working tree located in $HOME (a version-controlled project dir).
# Any other command that runs `rm` gets an explicit `ask`, which is the ordinary permission
# prompt and the only one `rm` gets (see lib-guarded-verb.sh); a command that runs no `rm` at
# all makes no decision (exit 0).
#
# The git-tree allowance trades on "this is a project under version control" being lower-stakes
# than a delete elsewhere — NOT on full recoverability: committed content is restorable via git,
# but untracked / .gitignore'd / uncommitted content, and an independent nested repo's history
# under a recursively-deleted parent, are NOT. Accepted as a deliberate convenience tradeoff.
# The command is read one segment at a time, so chaining and line breaks carry no weight of
# their own: `rm -f /tmp/a && rm -rf /tmp/b` is two deletes, each proved on its own operands.
# A decision covers the whole command line, so `allow` is emitted only when every segment is
# an `rm` this guard proved or a `cd` it could resolve. A line that mixes a proven `rm` with
# some other command makes no decision instead and leaves that line to the normal permission
# flow: the delete is not what needed a prompt, and waving the rest of the line through with
# it would turn a trailing `rm -f /tmp/x` into a way to auto-approve anything.
#
# Deny-by-default: every token must consist only of a safe character set (alphanumerics,
# `. _ / -` and glob chars `* ? [ ]`). That set contains none of the characters bash uses for
@@ -17,12 +20,12 @@
# and existing symlinks (so a symlink out of the allowed roots is caught), and a wildcard in a
# non-final path segment is refused because it can expand through a symlink realpath can't see.
#
# The git-repo allowance covers targets inside a git working tree under $HOME, and the tree's
# own root folder only when it is a linked worktree (`.git` is a pointer file, so history in
# the main repo survives); a primary checkout's root (`.git` is a history dir) and any `.git`,
# `.claude` or `.env` path are never auto-allowed. Globs auto-allow only under /tmp — elsewhere their expansion
# Which targets those two roots cover, and the tradeoff they rest on, is `path_class` in
# lib-guarded-verb.sh. Globs auto-allow only under /tmp — elsewhere their expansion
# could reach `.git` or a dotfile the literal checks never see. Relative operands resolve
# against the command's cwd (from the hook input).
# against the working directory the command runs from, which a `cd` in an earlier segment
# moves; once a `cd` is one this guard cannot resolve, that directory is unknown and a
# relative operand can no longer be proved.
#
# Assumes GNU `realpath` (-m) and `jq`, both present in this repo's Linux dev env.
set -uo pipefail
@@ -35,87 +38,103 @@ cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)
cwd=$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null)
# Every bail-out below goes through `defer`, so the forms this guard refuses to reason about —
# compound, quoted, wrapped — still reach the user as a prompt whenever an `rm` runs among them.
# wrapped, quoted, expanded — still reach the user as a prompt whenever an `rm` runs among them.
runs_verb rm "$cmd" && guarded=1 || guarded=0
defer() {
[ "$guarded" = 1 ] && decide ask "$1"
exit 0
}
# A newline separates commands, and the tokenizer below only reads the first line — defer.
case "$cmd" in *$'\n'*) defer "multi-line command" ;; esac
has_substitution "$cmd" && defer "command substitution in the command line"
read -r -a toks <<< "$cmd"
# Bare leading `rm` only; wrappers (`timeout rm`), env prefixes, and `/bin/rm` defer.
[ "${toks[0]:-}" = "rm" ] || defer "rm is not the leading command word"
# 0 (allow) iff the canonical path is an auto-allowable rm target: under /tmp, or strictly
# inside a git working tree located under $HOME. The walk stops at $HOME, so a dotfiles repo at
# ~ can't make all of $HOME deletable, and top-level ~ files stay protected.
allowed_target() {
local canon="$1" d root=""
case "$canon" in /tmp/?*) return 0 ;; esac
[ -n "${HOME:-}" ] || return 1
case "$canon" in "$HOME"/?*) ;; *) return 1 ;; esac
# Never auto-allow: history, and the two kinds of path the "it's under version control"
# premise doesn't hold for — the agent's own guards and settings (deleting them is what
# removes the prompt on everything else), and gitignored `.env` files.
case "$canon" in
*"/.git" | *"/.git/"* | *"/.claude" | *"/.claude/"*) return 1 ;;
*"/.env" | *"/.env."*) return 1 ;;
esac
d="$canon"
while [ "$d" != "/" ] && [ "$d" != "$HOME" ]; do
[ -e "$d/.git" ] && { root="$d"; break; }
d=$(dirname "$d")
# Proves one `rm` segment, whose tokens are in SEG_TOKS with `rm` at index 0, resolving relative
# operands against $seg_cwd. Returns only once every operand is an auto-allowable target;
# anything it cannot prove defers instead.
check_rm_segment() {
local i=1 t canon candidates had_operand=0 end_opts=0
while [ "$i" -lt "${#SEG_TOKS[@]}" ]; do
t="${SEG_TOKS[$i]}"
i=$((i + 1))
# Whitelist every token (flags included, so an operator hidden in a flag like `-rf;rm`
# can't slip past): any character outside the safe set makes it unsafe to reason about.
[ -n "$(printf '%s' "$t" | tr -d 'A-Za-z0-9._/*?[]-')" ] && defer "unsafe characters in \`$t\`"
# A glob in an option-looking token (`-[-]`) can expand to `--` and turn a later `-name`
# into an operand — never a real option, so defer.
case "$t" in -*[*?[]*) defer "glob inside the option \`$t\`" ;; esac
if [ "$end_opts" = 0 ]; then
[ "$t" = "--" ] && { end_opts=1; continue; }
# Skip real options only before the first operand. A bare `-` is a filename, and under
# POSIXLY_CORRECT GNU rm stops option parsing at the first operand, so a later `-name`
# is a filename too — validate it rather than skipping it.
if [ "$had_operand" = 0 ]; then
case "$t" in -?*) continue ;; esac
fi
fi
had_operand=1
# No wildcard in a non-final path segment (`a/*/b`): it can expand through a symlink
# realpath can't see. A slashless glob (`*.rs`) is a final-segment match — fine.
case "$t" in */*) case "${t%/*}" in *[*?[]*) defer "glob in a non-final segment of \`$t\`" ;; esac ;; esac
# A relative operand has as many candidate paths as the command has candidate working
# directories, and every one of them has to be auto-allowable: a `cd` that fails at runtime
# leaves the delete running in the directory it started in.
case "$t" in
/*) candidates=$(realpath -m -- "$t" 2>/dev/null) ;;
*) [ -n "$seg_cwd" ] || defer "\`$t\` is relative to a working directory this guard cannot pin down"
candidates=$(realpath -m -- "$seg_cwd/$t" 2>/dev/null)
[ -n "$alt_cwd" ] && candidates="$candidates
$(realpath -m -- "$alt_cwd/$t" 2>/dev/null)"
;;
esac
while IFS= read -r canon; do
[ -n "$canon" ] || defer "cannot resolve \`$t\`"
# A glob may auto-allow only under /tmp, where everything is deletable. Elsewhere its
# expansion could match `.git`, a dotfile like `.*`, or a nested checkout root that the
# literal-path checks never see — so require literal operands in git repos.
case "$t" in *[*?[]*) case "$canon" in /tmp/?*) ;; *) defer "glob \`$t\` is outside /tmp" ;; esac ;; esac
path_class "$canon" >/dev/null || defer "\`$canon\` is outside /tmp and not inside a git checkout in \$HOME"
done <<< "$candidates"
done
[ -n "$root" ] || return 1 # not inside a git working tree under $HOME
if [ "$canon" = "$root" ]; then
# Deleting the repo root folder itself: allow only for a linked worktree, whose `.git` is
# a file/pointer so the history lives in the main repo and survives. A primary checkout's
# `.git` is a directory holding the history, so deleting it is unrecoverable — defer.
[ -f "$root/.git" ] && return 0
return 1
fi
return 0
[ "$had_operand" = 1 ] || defer "no operand"
}
had_operand=0
end_opts=0
i=1
while [ "$i" -lt "${#toks[@]}" ]; do
t="${toks[$i]}"
i=$((i + 1))
# Whitelist every token (flags included, so an operator hidden in a flag like `-rf;rm`
# can't slip past): any character outside the safe set makes it unsafe to reason about.
[ -n "$(printf '%s' "$t" | tr -d 'A-Za-z0-9._/*?[]-')" ] && defer "unsafe characters in \`$t\`"
# A glob in an option-looking token (`-[-]`) can expand to `--` and turn a later `-name`
# into an operand — never a real option, so defer.
case "$t" in -*[*?[]*) defer "glob inside the option \`$t\`" ;; esac
if [ "$end_opts" = 0 ]; then
[ "$t" = "--" ] && { end_opts=1; continue; }
# Skip real options only before the first operand. A bare `-` is a filename, and under
# POSIXLY_CORRECT GNU rm stops option parsing at the first operand, so a later `-name`
# is a filename too — validate it rather than skipping it.
if [ "$had_operand" = 0 ]; then
case "$t" in -?*) continue ;; esac
fi
fi
had_operand=1
# No wildcard in a non-final path segment (`a/*/b`): it can expand through a symlink
# realpath can't see. A slashless glob (`*.rs`) is a final-segment match — fine.
case "$t" in */*) case "${t%/*}" in *[*?[]*) defer "glob in a non-final segment of \`$t\`" ;; esac ;; esac
case "$t" in
/*) canon=$(realpath -m -- "$t" 2>/dev/null) ;;
*) canon=$(realpath -m -- "${cwd:-$PWD}/$t" 2>/dev/null) ;;
split_segments "$cmd"
seg_cwd="${cwd:-$PWD}"
alt_cwd="" # where a `cd` that failed would have left the command
saw_cd=0
proved=0 # at least one `rm` segment came out auto-allowable
only_ours=1 # ... and nothing else shares the command line
for seg in "${SEGMENTS[@]}"; do
segment_tokens "$seg"
case "${SEG_TOKS[0]:-}" in
"") continue ;;
rm)
check_rm_segment
proved=1
continue
;;
cd)
# A `cd` writes nothing, so it never blocks an allow; it only moves where a later relative
# operand points, to one of the two candidates `apply_cd` describes.
if [ "$saw_cd" = 0 ] && new_cwd=$(apply_cd "$seg_cwd" "${SEG_TOKS[@]:1}"); then
alt_cwd="$seg_cwd"
seg_cwd="$new_cwd"
else
# Not the harmless segment an allow assumes: whatever this guard could not account for
# may be a redirect, and a redirect writes. Leave the line to the normal flow.
seg_cwd="" alt_cwd=""
only_ours=0
fi
saw_cd=1
continue
;;
esac
[ -n "$canon" ] || defer "cannot resolve \`$t\`"
# A glob may auto-allow only under /tmp, where everything is deletable. Elsewhere its
# expansion could match `.git`, a dotfile like `.*`, or a nested checkout root that the
# literal-path checks never see — so require literal operands in git repos.
case "$t" in *[*?[]*) case "$canon" in /tmp/?*) ;; *) defer "glob \`$t\` is outside /tmp" ;; esac ;; esac
allowed_target "$canon" || defer "\`$canon\` is outside /tmp and not inside a git checkout in \$HOME"
# Some other command shares the line. If an `rm` runs inside it after all — behind a wrapper,
# an env prefix or a path — this guard cannot say what it deletes.
segment_runs_verb rm "$seg" && defer "rm is not the leading command word in \`$seg\`"
only_ours=0
done
[ "$had_operand" = 1 ] || defer "no operand"
decide allow 'rm operands are under /tmp or inside a git checkout in $HOME'
[ "$proved" = 1 ] || exit 0
[ "$only_ours" = 1 ] && decide allow 'rm operands are under /tmp or inside a git checkout in $HOME'
exit 0
+152 -39
View File
@@ -10,17 +10,6 @@
# expand a glob operand against the filesystem. Neither guard relies on pathname expansion.
set -f
# 0 iff <verb> ($1) runs as a command word anywhere in <command> ($2). Mirrors how a Bash
# permission rule matches, so that owning the prompt here doesn't narrow what used to prompt:
# the command splits on `; & |` and newlines, and a leading env assignment or process wrapper
# (`timeout 5 rm`, `xargs rm`) is skipped before the command word is read.
#
# The split set also carries the characters that open a nested command — `$(`, backticks and
# `( )` — because a rule matches the verb inside one (`echo $(rm -rf ~)` prompts), and a
# separator that only ends statements would read that as an `echo`. Braces are handled as
# words rather than separators, since splitting on them cuts `xargs -I {} … rm` in half and
# strands the `rm` in a segment that no longer knows a wrapper preceded it.
# 0 iff <text> ($1) starts with a command that only reads its input. An allowlist, because the
# opposite — naming the shells to avoid — would have to be complete: an unlisted one (`ash`,
# `rbash`, `busybox sh`) executes the body while the guard calls it data. Unrecognized here only
@@ -115,35 +104,159 @@ strip_heredoc_bodies() {
done
}
# 0 iff <verb> ($1) runs as a command word in <segment> ($2), which must already be one
# segment (no separator left in it). Wrapper, env-prefix and `/bin/<verb>` forms all count.
segment_runs_verb() {
local verb="$1" w wrapped=0
for w in $2; do
# The shell strips quotes and backslashes before it looks up the command, so `'rm'` and
# `r\m` run rm and have to compare equal to it.
w="${w//[\"\'\\]/}"
case "$w" in
"$verb" | */"$verb") return 0 ;;
*=*) ;; # leading env assignment
-* | *'>'* | *'<'*) ;; # a flag, or a leading redirect
[0-9]*) [ "$wrapped" = 1 ] || break ;; # a wrapper's duration, not `1:` in prose
'!' | '{' | '}' | if | then | elif | else | while | until | do) ;; # never the command
timeout | time | nice | nohup | stdbuf | command | builtin | noglob | xargs | sudo | env)
wrapped=1 ;;
# A wrapper's option value is indistinguishable from a command name (`stdbuf -o L rm`),
# so past a wrapper the scan runs to the end of the segment instead of stopping at the
# first ordinary word. Before one, that word is the command and the verb cannot follow
# it. Nothing bounds the scan: a wrapper takes unboundedly many operands
# (`env -u A -u B ...`), and any cutoff — a word count, or stopping at the first quoted
# word — drops the prompt for a real `sudo -u 'root' rm`. Prose after a wrapper is the
# price, and it only over-prompts.
*) [ "$wrapped" = 1 ] || break ;;
esac
done
return 1
}
# Splits <command> ($1) into its command segments, into the global array SEGMENTS. Every guard
# reasons one segment at a time, so `a && b` is two commands here rather than one unparsable
# blob, and a newline is a separator like any other.
#
# The split set carries more than `; & |` and newlines: `$(`, backticks and `( )` open a nested
# command, and a separator that only ended statements would read `echo $(rm -rf ~)` as an
# `echo`. Braces are handled as words rather than separators, since splitting on them cuts
# `xargs -I {} … rm` in half and strands the `rm` in a segment that no longer knows a wrapper
# preceded it.
#
# `tr` and not `${1//[...]}`: a `}` inside the bracket expression closes the expansion itself,
# which silently leaves the command unsplit and every separator unseen.
split_segments() {
local seg
SEGMENTS=()
while IFS= read -r seg; do SEGMENTS+=("$seg"); done <<< "$(strip_heredoc_bodies "$1" | tr ';&|()`' '\n')"
}
# 0 iff <command> ($1) carries a command substitution outside a heredoc body. A substitution is
# concatenated into the word it sits in, and splitting on its opener cuts that word in half:
# `/tmp/a/`printf ../../etc`` would be proved as `/tmp/a/`, with the traversal validated as an
# unrelated segment. Nothing here can evaluate it, so a guard proves nothing about such a
# command. Heredoc bodies are excepted — those are data the split has already dropped.
has_substitution() {
case "$(strip_heredoc_bodies "$1")" in
*'$('* | *'`'*) return 0 ;;
esac
return 1
}
# Reads <segment> ($1) into the global array SEG_TOKS, dropping the shell keywords that can
# precede a command word so that `then rm -rf x` is analyzed as the `rm` it runs. Word
# splitting only: quotes are left in the token and fail the guards' charset check downstream,
# which is what keeps `rm -rf "$HOME/x"` unprovable.
segment_tokens() {
SEG_TOKS=()
read -r -a SEG_TOKS <<< "$1"
while [ "${#SEG_TOKS[@]}" -gt 0 ]; do
case "${SEG_TOKS[0]}" in
'!' | '{' | '}' | if | then | elif | else | while | until | do) SEG_TOKS=("${SEG_TOKS[@]:1}") ;;
*) break ;;
esac
done
}
# Prints the directory a `cd` lands in, given the current one ($1) and the tokens after the
# `cd` ($2...). Fails, printing nothing, when the destination cannot be resolved — a variable,
# `-`, an option, a relative path, no operand at all (`cd` alone is $HOME), or more than one.
#
# Resolving says nothing about whether the `cd` will SUCCEED: the destination may not exist, and
# `;` runs the next command anyway, leaving it in the directory it started in. So a caller may
# never treat this as the working directory outright — it is one of two candidates, and a
# relative operand has to be provable against the one the command started in as well. That also
# makes a `cd` word splitting invented out of quoted text harmless: it can only add a candidate,
# never drop one. Past the first `cd` the branching outruns two candidates, so a caller that
# sees a second gives up on relative operands entirely.
apply_cd() {
local cwd="$1" t
shift
[ "$#" -eq 1 ] || return 1
t="$1"
[ -n "$(printf '%s' "$t" | tr -d 'A-Za-z0-9._/-')" ] && return 1
# Absolute only. A relative destination is not `$cwd/$t`: the shell searches $CDPATH first,
# so `cd ssh` may land in /etc/ssh, and this cannot see the caller's $CDPATH to rule it out.
case "$t" in /*) ;; *) return 1 ;; esac
realpath -m -- "$t" 2>/dev/null
}
# Prints the class of a canonical path and returns 0: `tmp` for one strictly under /tmp, or
# `repo:<root>` for one strictly inside the git working tree at <root>, itself under $HOME.
# Fails, printing nothing, for anything else — those are the only roots the guards are willing
# to touch unprompted. The root is part of the class so that a caller pairing two operands can
# tell one checkout from another: sibling repos are separate permission boundaries, not one.
#
# The `repo` class trades on "this is a project under version control" being lower-stakes than
# the same act elsewhere — NOT on full recoverability: committed content is restorable via git,
# but untracked / .gitignore'd / uncommitted content, and an independent nested repo's history
# under a recursively-deleted parent, are NOT. Accepted as a deliberate convenience tradeoff.
#
# The walk stops at $HOME, so a dotfiles repo at ~ can't put all of $HOME in a class, and
# top-level ~ files stay out of one. A working tree's own root folder counts only when it is a
# linked worktree, whose `.git` is a pointer file so the history lives in the main repo and
# survives; a primary checkout's `.git` is a directory holding the history itself, so losing it
# is unrecoverable.
#
# Some paths are in no class in any root, /tmp included. Git history, and the agent's own guards
# and settings, because removing those is what removes the prompt on everything else. And every
# path `.claude/settings.json` refuses to read — `.env`, `secrets/`, `*.pem`, `*.key`,
# `credentials.json`, `.secret*` — because a `cp` or `mv` that is auto-allowed on both ends
# would rename one out of those globs and hand back through `Read` exactly what they deny.
path_class() {
local canon="$1" d root=""
case "$canon" in
*"/.git" | *"/.git/"* | *"/.claude" | *"/.claude/"*) return 1 ;;
*"/.env" | *"/.env."*) return 1 ;;
*"/secrets" | *"/secrets/"*) return 1 ;;
*.pem | *.key | *"/credentials.json") return 1 ;;
*"/.secret"* | *.secret | *.secrets) return 1 ;;
esac
case "$canon" in /tmp/?*) printf 'tmp'; return 0 ;; esac
[ -n "${HOME:-}" ] || return 1
case "$canon" in "$HOME"/?*) ;; *) return 1 ;; esac
d="$canon"
while [ "$d" != "/" ] && [ "$d" != "$HOME" ]; do
[ -e "$d/.git" ] && { root="$d"; break; }
d=$(dirname "$d")
done
[ -n "$root" ] || return 1 # not inside a git working tree under $HOME
if [ "$canon" = "$root" ]; then
[ -f "$root/.git" ] || return 1
fi
printf 'repo:%s' "$root"
}
# 0 iff <verb> ($1) runs as a command word anywhere in <command> ($2). Mirrors how a Bash
# permission rule matches, so that owning the prompt here doesn't narrow what used to prompt:
# a guard consults this before it starts proving segments, and every bail-out it then takes
# is a prompt for exactly the commands a rule would have caught.
runs_verb() {
local verb="$1" seg w wrapped
while IFS= read -r seg; do
wrapped=0
for w in $seg; do
# The shell strips quotes and backslashes before it looks up the command, so `'rm'` and
# `r\m` run rm and have to compare equal to it.
w="${w//[\"\'\\]/}"
case "$w" in
"$verb" | */"$verb") return 0 ;;
*=*) ;; # leading env assignment
-* | *'>'* | *'<'*) ;; # a flag, or a leading redirect
[0-9]*) [ "$wrapped" = 1 ] || break ;; # a wrapper's duration, not `1:` in prose
'!' | '{' | '}' | if | then | elif | else | while | until | do) ;; # never the command
timeout | time | nice | nohup | stdbuf | command | builtin | noglob | xargs | sudo | env)
wrapped=1 ;;
# A wrapper's option value is indistinguishable from a command name (`stdbuf -o L rm`),
# so past a wrapper the scan runs to the end of the segment instead of stopping at the
# first ordinary word. Before one, that word is the command and the verb cannot follow
# it. Nothing bounds the scan: a wrapper takes unboundedly many operands
# (`env -u A -u B …`), and any cutoff — a word count, or stopping at the first quoted
# word — drops the prompt for a real `sudo -u 'root' rm`. Prose after a wrapper is the
# price, and it only over-prompts.
*) [ "$wrapped" = 1 ] || break ;;
esac
done
# `tr` and not `${2//[...]}`: a `}` inside the bracket expression closes the expansion
# itself, which silently leaves the command unsplit and every separator unseen.
done <<< "$(strip_heredoc_bodies "$2" | tr ';&|()`' '\n')"
local verb="$1" seg
split_segments "$2"
for seg in "${SEGMENTS[@]}"; do
segment_runs_verb "$verb" "$seg" && return 0
done
return 1
}
+78 -2
View File
@@ -4,6 +4,10 @@
# What this pins is the `ask` column: a matcher change that turns one into a no-decision drops
# that command's only prompt (see lib-guarded-verb.sh). The wrapper, nested-command and quoted
# rows are the ones that catch it.
#
# The `allow` column carries its own weight, because a decision covers the whole command line:
# `allow` may only appear where every segment was proved here, and a line that also runs
# something unexamined has to come out `none` so the normal permission flow still sees it.
set -uo pipefail
H="$(cd "${BASH_SOURCE[0]%/*}" && pwd)"
CWD="$(git -C "$H" rev-parse --show-toplevel)"
@@ -46,10 +50,11 @@ run $G ask "rm -rf $CWD/*"
run $G ask "rm -rf /etc/passwd"
run $G ask 'rm -rf "$HOME/x"'
run $G ask "rm -rf /tmp/../$OUT"
run $G ask "ls /tmp && rm -rf /tmp/x"
run $G none "ls /tmp && rm -rf /tmp/x" # proved delete, unexamined neighbour
run $G ask 'echo $(rm -rf /etc)'
run $G ask 'echo `rm -rf /etc`'
run $G ask "{ rm -rf /etc; }"
run $G allow "{ rm -rf /tmp/scratch/x; }" # the keyword drops, the delete still proves
run $G ask "find . -name x | xargs rm"
run $G ask "timeout 5 rm -rf /tmp/x"
run $G ask "stdbuf -o L rm -rf /etc"
@@ -107,6 +112,33 @@ run $G none 'echo $(ls /tmp)'
run $G none 'grep -rn "rm" backend/'
run $G none "cargo build --release"
# Chaining and line breaks are not themselves a reason to prompt: each segment is proved on its
# own operands, and a `cd` moves where a relative one points.
run $G allow "rm -f /tmp/a; rm -rf /tmp/b"
run $G allow "$(printf 'rm -f /tmp/a\nrm -rf %s/frontend/scratch' "$CWD")"
run $G allow "cd /tmp/scratch && rm -rf sub"
run $G none "mkdir -p /tmp/x && rm -rf /tmp/x"
run $G ask "$(printf 'ls /tmp\nrm -rf /etc')"
# A `cd` this guard can resolve is where the relative operand lands; one it cannot leaves the
# working directory unknown, and an unknown one proves nothing.
run $G ask "cd /etc && rm -rf foo"
run $G ask 'cd "$D" && rm -rf foo'
run $G ask "cd $CWD && rm -rf .git"
run $G ask "cd /etc && cd /tmp/scratch && rm -rf sub" # a cd out is not walked back
# A `cd` can fail at runtime, and `;` runs the delete from where the command started, so a
# relative operand is proved from both directories.
run $G ask "cd /tmp/does-not-exist; rm -rf .git"
run $G ask "cd /tmp/does-not-exist; rm -rf backend/.env"
run $G ask "cd /tmp/a && cd /tmp/b && rm -rf sub"
run $G ask "rm -rf /tmp/clone/.git" # history is never in a class
run $G ask "rm -rf /tmp/scratch/id_rsa.key"
run $G none "cd /tmp >$OUT; rm -f /tmp/a"
# A substitution is concatenated into its word, so splitting on it would prove only the literal
# half; a relative `cd` is not $cwd/$t either, since the shell searches $CDPATH first.
run $G ask 'rm -rf /tmp/a/`printf ../../etc`'
run $G ask 'rm -rf /tmp/a/$(printf ../../etc)'
run $G ask "cd ssh && rm -rf moduli"
echo
echo "== allow-fileops-in-tmp.sh =="
A=allow-fileops-in-tmp.sh
@@ -117,7 +149,7 @@ run $A allow "tar -xzf /tmp/a.tar.gz -C /tmp/out"
run $A ask "mv /tmp/a $OUT"
run $A ask "mv $CWD/AGENTS.md /tmp/a"
run $A ask "chmod -R 777 $CWD"
run $A ask "ls && mv /tmp/a /tmp/b"
run $A none "ls && mv /tmp/a /tmp/b" # proved move, unexamined neighbour
run $A ask 'echo $(mv /tmp/a /etc)'
run $A ask "timeout --signal KILL 5 mv /tmp/a /etc"
run $A ask "time -f FORMAT chmod 777 $OUT"
@@ -129,5 +161,49 @@ run $A none "cp $CWD/AGENTS.md /tmp/a"
run $A none "tar -xzf /tmp/a.tar.gz -C $OUT"
run $A none "cargo build"
run $A none "mkdir -p /tmp/x; mv /tmp/a /tmp/x; chmod 755 /tmp/x" # one write per line
run $A none "$(printf 'mv /tmp/a /tmp/b\nchmod 755 /tmp/b')"
run $A ask "ls && mv /tmp/a /etc"
run $A ask "$(printf 'mkdir -p /tmp/x\nchmod -R 777 %s' "$CWD")"
run $A allow "cd /tmp/x && tar -xzf /tmp/a.tar.gz -C /tmp/out"
# The checkout is a root of its own, so an in-repo move or chmod is as auto-allowable as the
# in-repo delete already was — but one operation may not straddle it and /tmp.
run $A allow "chmod +x scripts/worktree-env"
run $A allow "mv backend/.sqlx backend/.sqlx.bad"
run $A allow "mv $CWD/frontend/a.ts $CWD/frontend/b.ts"
run $A ask "mv /tmp/a $CWD/frontend/a.ts"
run $A ask "chmod -R 777 $CWD/.git"
run $A ask "mv $CWD/backend/.env $CWD/backend/.env.bak"
run $A ask "mv $CWD/AGENTS.md $OUT"
run $A ask "cd /etc && mv a b"
# An auto-allowed rename may not carry a path out of the `Read` deny globs.
run $A ask "mv backend/server.pem backend/server.txt"
run $A none "cp backend/secrets/token frontend/token.txt" # cp has no prompt of its own,
# so what matters is it is not allowed
run $A ask "mv $CWD/backend/credentials.json /tmp/x"
run $A ask "cd /tmp/does-not-exist; mv .claude/settings.json settings.bak"
# A segment this hook cannot read whole may carry a redirect, and an earlier write can change
# what a later operand resolves to — neither may ride along on an allow.
run $A none "cd /tmp >$OUT; mv /tmp/a /tmp/b"
run $A none "cp -r /tmp/tree /tmp/live; cp /tmp/payload /tmp/live/link"
run $A ask 'mv /tmp/a/`printf ../../etc/x` /tmp/b'
# A sibling checkout is a different root: its files are outside what the Read tool is confined
# to, and copying them in would hand back what that confinement withholds.
EE="$(dirname "$CWD")/windmill-ee-private" # a sibling checkout; absent elsewhere, still not a root
run $A ask "mv $EE/backend/x.rs $CWD/backend/x.rs"
run $A none "cp $EE/README.md $CWD/README.copy"
# Directory form writes a path the command does not name — DEST/basename(SRC) — and `cp`
# follows that child when it is a symlink, as every `*_ee.rs` in this checkout is.
run $A ask "mv frontend/apps_ee.rs backend/windmill-api/src"
run $A none "cp frontend/apps_ee.rs backend/windmill-api/src"
run $A none "cp frontend/a.ts backend"
run $A ask "mv /tmp/a $CWD/backend"
# ... and a `cd` that fails at runtime may not hide that form: the destination is a directory
# in the directory the command actually ran in, whichever of the two that turns out to be.
run $A none "cd $CWD/AGENTS.md; cp frontend/apps_ee.rs backend/windmill-api/src"
run $A ask "cd $CWD/AGENTS.md; mv frontend/apps_ee.rs backend/windmill-api/src"
run $A none "cd /tmp/x && tar -xzf /tmp/a.tar.gz" # no -C, and the cwd is now two candidates
run $A allow "cp frontend/a.ts backend/a.ts" # ... naming the destination proves fine
echo
[ "$fails" = 0 ] && echo "ALL PASS" || { echo "$fails FAILURES"; exit 1; }
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "1.791.0"
".": "1.792.1"
}
+13 -4
View File
@@ -147,10 +147,19 @@ $NAV --root backend callees "X" # what does X call?
- **MUST `outline` before `Read`** on unfamiliar files — then `body` or `Read` with offset/limit for specifics
- **Scratch stays outside the checkout.** Temp scripts, data dumps, cache backups and
screenshots go in the session scratch directory or `/tmp`, so nothing temporary can end up
committed. Write `rm`/`mv`/`cp` as one plain unchained command: a PreToolUse hook
auto-allows those when every operand is under `/tmp` or inside this checkout, but it defers
on `&&`, `;`, redirects, quotes and `$VAR` — that deferral, not the delete itself, is what
turns a routine cleanup into a permission prompt.
committed. Write the paths in `rm`/`mv`/`cp` out literally: a PreToolUse hook proves each
operand, and auto-allows deletes, moves, copies and mode changes under `/tmp` or inside a git
checkout under `$HOME`, as long as one operation stays within a single root — a sibling
checkout is a root of its own (`tar` and `unzip` stay `/tmp`-only). Chain deletes freely, each
proved on its own operands, but keep writes to one per line, name the destination rather than
a directory to drop it in, and put anything else on its own line: a command the hook does not
prove drops the whole line back to the normal permission flow. A
quoted or `$VAR` operand, a `~`, a redirect, a `$(…)`, a relative `cd`, or a wrapper like
`xargs rm` cannot be proved, and that deferral is what turns a cleanup into a prompt.
- **Change files with Edit/Write, not the shell.** `sed -i`, `cat > file <<'EOF'` and inline
`python3 - <<'PY'` scripts put an edit through the PreToolUse guards and the permission
classifier, which match `Bash` and nothing else, so a routine edit arrives as a prompt. Bash
stays right for running things — tests, builds, git, one-off queries.
- Search for existing code to reuse before writing new code
- Follow established patterns in the codebase
- Keep changes focused — don't refactor beyond what's asked
+22
View File
@@ -1,5 +1,27 @@
# Changelog
## [1.792.1](https://github.com/windmill-labs/windmill/compare/v1.792.0...v1.792.1) (2026-08-18)
### Bug Fixes
* route legacy AI entry points to sessions instead of the unmounted chat ([#10705](https://github.com/windmill-labs/windmill/issues/10705)) ([494e6f1](https://github.com/windmill-labs/windmill/commit/494e6f146e6a22bd498db58fa10c7866cd56dc4d))
## [1.792.0](https://github.com/windmill-labs/windmill/compare/v1.791.0...v1.792.0) (2026-08-18)
### Features
* **frontend:** record the outcome of every AI chat tool call ([#10746](https://github.com/windmill-labs/windmill/issues/10746)) ([7b17e35](https://github.com/windmill-labs/windmill/commit/7b17e358b35bf4ef8c213ea13252a456e87acb32))
### Bug Fixes
* **api:** document cache_ignore_s3_path on the Script read schema ([#10742](https://github.com/windmill-labs/windmill/issues/10742)) ([6783a39](https://github.com/windmill-labs/windmill/commit/6783a396b144948fa60324eae888bc4a83917bc8))
* audit the icon library against brand guidelines ([#10722](https://github.com/windmill-labs/windmill/issues/10722)) ([6749015](https://github.com/windmill-labs/windmill/commit/6749015fbf7afe0c6dcd53b1933b0152915afd32))
* **cli:** keep script settings on push and repair the up-to-date check ([#10741](https://github.com/windmill-labs/windmill/issues/10741)) ([ef4dc46](https://github.com/windmill-labs/windmill/commit/ef4dc46d4bfd00e583a39e5c053c0022f1ad3abd))
* show runtime-detected assets in a run's Assets tab ([#10738](https://github.com/windmill-labs/windmill/issues/10738)) ([1fa3bf3](https://github.com/windmill-labs/windmill/commit/1fa3bf3b291c32ffd62f77df59e24993afb7c78a))
## [1.791.0](https://github.com/windmill-labs/windmill/compare/v1.790.1...v1.791.0) (2026-08-17)
+81 -81
View File
@@ -14665,7 +14665,7 @@ dependencies = [
[[package]]
name = "windmill"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-nats",
@@ -14750,7 +14750,7 @@ dependencies = [
[[package]]
name = "windmill-ai"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"async-stream",
"async-trait",
@@ -14783,7 +14783,7 @@ dependencies = [
[[package]]
name = "windmill-alerting"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -14796,7 +14796,7 @@ dependencies = [
[[package]]
name = "windmill-api"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"argon2",
@@ -14936,7 +14936,7 @@ dependencies = [
[[package]]
name = "windmill-api-agent-workers"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -14959,7 +14959,7 @@ dependencies = [
[[package]]
name = "windmill-api-assets"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -14976,7 +14976,7 @@ dependencies = [
[[package]]
name = "windmill-api-auth"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"axum 0.8.9",
@@ -15002,7 +15002,7 @@ dependencies = [
[[package]]
name = "windmill-api-client"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"reqwest 0.12.28",
"serde",
@@ -15012,7 +15012,7 @@ dependencies = [
[[package]]
name = "windmill-api-configs"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -15029,7 +15029,7 @@ dependencies = [
[[package]]
name = "windmill-api-debug"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"axum 0.8.9",
"base64 0.22.1",
@@ -15051,7 +15051,7 @@ dependencies = [
[[package]]
name = "windmill-api-embeddings"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"axum 0.8.9",
@@ -15074,7 +15074,7 @@ dependencies = [
[[package]]
name = "windmill-api-flow-conversations"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -15090,7 +15090,7 @@ dependencies = [
[[package]]
name = "windmill-api-flows"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -15112,7 +15112,7 @@ dependencies = [
[[package]]
name = "windmill-api-groups"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -15133,7 +15133,7 @@ dependencies = [
[[package]]
name = "windmill-api-inputs"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -15147,7 +15147,7 @@ dependencies = [
[[package]]
name = "windmill-api-integration-tests"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-nats",
@@ -15182,7 +15182,7 @@ dependencies = [
[[package]]
name = "windmill-api-jobs"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"axum 0.8.9",
@@ -15207,7 +15207,7 @@ dependencies = [
[[package]]
name = "windmill-api-npm-proxy"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -15235,7 +15235,7 @@ dependencies = [
[[package]]
name = "windmill-api-openapi"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"axum 0.8.9",
@@ -15257,7 +15257,7 @@ dependencies = [
[[package]]
name = "windmill-api-schedule"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -15277,7 +15277,7 @@ dependencies = [
[[package]]
name = "windmill-api-scripts"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -15315,7 +15315,7 @@ dependencies = [
[[package]]
name = "windmill-api-settings"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"axum 0.8.9",
@@ -15343,7 +15343,7 @@ dependencies = [
[[package]]
name = "windmill-api-sse"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"lazy_static",
"serde",
@@ -15355,7 +15355,7 @@ dependencies = [
[[package]]
name = "windmill-api-users"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"argon2",
"axum 0.8.9",
@@ -15379,7 +15379,7 @@ dependencies = [
[[package]]
name = "windmill-api-workers"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -15393,7 +15393,7 @@ dependencies = [
[[package]]
name = "windmill-api-workspaces"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"axum 0.8.9",
"chrono",
@@ -15428,7 +15428,7 @@ dependencies = [
[[package]]
name = "windmill-audit"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"chrono",
"lazy_static",
@@ -15442,7 +15442,7 @@ dependencies = [
[[package]]
name = "windmill-autoscaling"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"axum 0.8.9",
@@ -15461,7 +15461,7 @@ dependencies = [
[[package]]
name = "windmill-common"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"aes-gcm",
"aho-corasick",
@@ -15565,7 +15565,7 @@ dependencies = [
[[package]]
name = "windmill-dep-map"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"chrono",
"itertools 0.14.0",
@@ -15584,7 +15584,7 @@ dependencies = [
[[package]]
name = "windmill-git-sync"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"regex",
"serde",
@@ -15599,7 +15599,7 @@ dependencies = [
[[package]]
name = "windmill-indexer"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"astral-tokio-tar",
@@ -15623,7 +15623,7 @@ dependencies = [
[[package]]
name = "windmill-jseval"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"futures",
@@ -15640,7 +15640,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"itertools 0.14.0",
"lazy_static",
@@ -15656,7 +15656,7 @@ dependencies = [
[[package]]
name = "windmill-mcp"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-trait",
@@ -15677,7 +15677,7 @@ dependencies = [
[[package]]
name = "windmill-native-triggers"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-trait",
@@ -15708,7 +15708,7 @@ dependencies = [
[[package]]
name = "windmill-oauth"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"arc-swap",
@@ -15733,7 +15733,7 @@ dependencies = [
[[package]]
name = "windmill-object-store"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-stream",
@@ -15767,7 +15767,7 @@ dependencies = [
[[package]]
name = "windmill-operator"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"futures",
@@ -15785,7 +15785,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"convert_case 0.6.0",
"serde",
@@ -15794,7 +15794,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -15806,7 +15806,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"serde_json",
@@ -15818,7 +15818,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"gosyn",
@@ -15830,7 +15830,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -15842,7 +15842,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"serde_json",
@@ -15854,7 +15854,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"nu-parser",
@@ -15865,7 +15865,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15876,7 +15876,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -15888,7 +15888,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-asset"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -15899,7 +15899,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -15921,7 +15921,7 @@ dependencies = [
[[package]]
name = "windmill-parser-r"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"serde_json",
@@ -15933,7 +15933,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -15947,7 +15947,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"convert_case 0.6.0",
@@ -15964,7 +15964,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -15977,7 +15977,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql-asset"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"serde",
@@ -15989,7 +15989,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -16007,7 +16007,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts-asset"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"serde-wasm-bindgen",
@@ -16023,7 +16023,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wac"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -16039,7 +16039,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -16053,7 +16053,7 @@ dependencies = [
[[package]]
name = "windmill-queue"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -16092,7 +16092,7 @@ dependencies = [
[[package]]
name = "windmill-runtime-nativets"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"const_format",
@@ -16132,7 +16132,7 @@ dependencies = [
[[package]]
name = "windmill-sql-datatype-parser-wasm"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"getrandom 0.3.4",
"wasm-bindgen",
@@ -16143,7 +16143,7 @@ dependencies = [
[[package]]
name = "windmill-store"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -16178,7 +16178,7 @@ dependencies = [
[[package]]
name = "windmill-test-utils"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-trait",
@@ -16202,7 +16202,7 @@ dependencies = [
[[package]]
name = "windmill-trigger"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-trait",
@@ -16235,7 +16235,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-amqp"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-trait",
@@ -16262,7 +16262,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-azure"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-trait",
@@ -16295,7 +16295,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-email"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-trait",
@@ -16315,7 +16315,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-gcp"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-trait",
@@ -16349,7 +16349,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-http"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-trait",
@@ -16385,7 +16385,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-kafka"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-trait",
@@ -16408,7 +16408,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-mqtt"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-trait",
@@ -16432,7 +16432,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-nats"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-nats",
@@ -16456,7 +16456,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-postgres"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-trait",
@@ -16491,7 +16491,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-sqs"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-trait",
@@ -16519,7 +16519,7 @@ dependencies = [
[[package]]
name = "windmill-trigger-websocket"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-trait",
@@ -16544,7 +16544,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"bitflags 2.13.1",
@@ -16563,7 +16563,7 @@ dependencies = [
[[package]]
name = "windmill-worker"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-once-cell",
@@ -16679,7 +16679,7 @@ dependencies = [
[[package]]
name = "windmill-worker-volumes"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"bytes",
"futures",
@@ -17462,9 +17462,9 @@ dependencies = [
[[package]]
name = "zerovec-derive"
version = "0.11.4"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47402523226a02bfe5230160dc3ccc089aa6f6f19e7fcbb4e6f824bbb1b4aa62"
checksum = "9f212a141d820099d57ffafb9569be9617a6f27d3dc881fbee8fb56642f917a9"
dependencies = [
"proc-macro2",
"quote",
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "windmill"
version = "1.791.0"
version = "1.792.1"
authors.workspace = true
edition.workspace = true
@@ -88,7 +88,7 @@ members = [
exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"]
[workspace.package]
version = "1.791.0"
version = "1.792.1"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
edition = "2021"
+24 -24
View File
@@ -6191,7 +6191,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windmill-common"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"aho-corasick",
"anyhow",
@@ -6274,7 +6274,7 @@ dependencies = [
[[package]]
name = "windmill-macros"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"proc-macro2",
"quote",
@@ -6286,7 +6286,7 @@ dependencies = [
[[package]]
name = "windmill-parser"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"convert_case",
"serde",
@@ -6295,7 +6295,7 @@ dependencies = [
[[package]]
name = "windmill-parser-bash"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -6307,7 +6307,7 @@ dependencies = [
[[package]]
name = "windmill-parser-csharp"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"serde_json",
@@ -6319,7 +6319,7 @@ dependencies = [
[[package]]
name = "windmill-parser-go"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"gosyn",
@@ -6331,7 +6331,7 @@ dependencies = [
[[package]]
name = "windmill-parser-graphql"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -6343,7 +6343,7 @@ dependencies = [
[[package]]
name = "windmill-parser-java"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"serde_json",
@@ -6355,7 +6355,7 @@ dependencies = [
[[package]]
name = "windmill-parser-nu"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"nu-parser",
@@ -6366,7 +6366,7 @@ dependencies = [
[[package]]
name = "windmill-parser-php"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -6377,7 +6377,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"itertools 0.14.0",
@@ -6389,7 +6389,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-asset"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -6400,7 +6400,7 @@ dependencies = [
[[package]]
name = "windmill-parser-py-imports"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"async-recursion",
@@ -6422,7 +6422,7 @@ dependencies = [
[[package]]
name = "windmill-parser-r"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"serde_json",
@@ -6434,7 +6434,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ruby"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -6448,7 +6448,7 @@ dependencies = [
[[package]]
name = "windmill-parser-rust"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"convert_case",
@@ -6465,7 +6465,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -6478,7 +6478,7 @@ dependencies = [
[[package]]
name = "windmill-parser-sql-asset"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"serde",
@@ -6490,7 +6490,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -6508,7 +6508,7 @@ dependencies = [
[[package]]
name = "windmill-parser-ts-asset"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"serde-wasm-bindgen",
@@ -6524,7 +6524,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wac"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"rustpython-ast",
@@ -6540,7 +6540,7 @@ dependencies = [
[[package]]
name = "windmill-parser-wasm"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"getrandom 0.2.17",
@@ -6572,7 +6572,7 @@ dependencies = [
[[package]]
name = "windmill-parser-yaml"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"lazy_static",
@@ -6586,7 +6586,7 @@ dependencies = [
[[package]]
name = "windmill-types"
version = "1.791.0"
version = "1.792.1"
dependencies = [
"anyhow",
"bitflags",
@@ -12,7 +12,7 @@ resolver = "2"
members = ["."]
[workspace.package]
version = "1.791.0"
version = "1.792.1"
edition = "2021"
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
+118
View File
@@ -0,0 +1,118 @@
//! Pins the `parent_hash` filter of `GET /w/{workspace}/scripts/list`: the
//! request must succeed and return exactly the scripts whose `parent_hashes`
//! array contains the given hash. The filter is assembled into a raw SQL string,
//! so a malformed predicate is only caught by executing the query.
use serde_json::json;
use sqlx::{Pool, Postgres};
use windmill_test_utils::*;
fn client() -> reqwest::Client {
reqwest::Client::new()
}
fn authed(builder: reqwest::RequestBuilder, token: &str) -> reqwest::RequestBuilder {
builder.header("Authorization", format!("Bearer {}", token))
}
fn new_script(path: &str, content: &str) -> serde_json::Value {
json!({
"path": path,
"summary": "",
"description": "",
"content": content,
"language": "deno",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {},
"required": []
}
})
}
async fn create(port: u16, script: serde_json::Value) -> anyhow::Result<String> {
let resp = authed(
client().post(format!(
"http://localhost:{port}/api/w/test-workspace/scripts/create"
)),
"SECRET_TOKEN",
)
.json(&script)
.send()
.await?;
let status = resp.status();
let body = resp.text().await?;
assert_eq!(status, 201, "script create should succeed: {body}");
Ok(body)
}
/// Paths returned by `scripts/list` for the given query string.
async fn list_paths(port: u16, query: &str) -> anyhow::Result<Vec<String>> {
let resp = authed(
client().get(format!(
"http://localhost:{port}/api/w/test-workspace/scripts/list?{query}"
)),
"SECRET_TOKEN",
)
.send()
.await?;
let status = resp.status();
let body = resp.text().await?;
assert_eq!(status, 200, "scripts/list?{query} should succeed: {body}");
let items: Vec<serde_json::Value> = serde_json::from_str(&body)?;
Ok(items
.iter()
.map(|it| it["path"].as_str().unwrap().to_string())
.collect())
}
#[sqlx::test(fixtures("base"))]
async fn test_list_scripts_parent_hash_filter(db: Pool<Postgres>) -> anyhow::Result<()> {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await?;
let port = server.addr.port();
let lineage_path = "u/test-user/parent_hash_lineage";
let unrelated_path = "u/test-user/parent_hash_unrelated";
// A two-version lineage: v2's `parent_hashes` contains v1's hash.
let v1 = create(
port,
new_script(lineage_path, "export async function main() { return 1; }"),
)
.await?;
let mut v2_body = new_script(lineage_path, "export async function main() { return 2; }");
v2_body["parent_hash"] = json!(v1);
let v2 = create(port, v2_body).await?;
// ...plus a script with no lineage at all, which must never match.
create(
port,
new_script(unrelated_path, "export async function main() { return 3; }"),
)
.await?;
let all = list_paths(port, "").await?;
assert!(
all.contains(&lineage_path.to_string()) && all.contains(&unrelated_path.to_string()),
"unfiltered listing should return both scripts, got {all:?}"
);
let descendants = list_paths(port, &format!("parent_hash={v1}")).await?;
assert_eq!(
descendants,
vec![lineage_path.to_string()],
"parent_hash should return only the scripts descending from that hash"
);
// v2 is the head, so nothing descends from it.
let none = list_paths(port, &format!("parent_hash={v2}")).await?;
assert!(
none.is_empty(),
"no script descends from the head version, got {none:?}"
);
Ok(())
}
+3 -1
View File
@@ -289,7 +289,9 @@ async fn list_scripts(
sqlb.and_where_eq("parent_hashes[array_upper(parent_hashes, 1)]", &ph.0);
}
if let Some(ph) = &lq.parent_hash {
sqlb.and_where_eq("any(parent_hashes)", &ph.0);
// ANY() only ever sits on the right of the comparison; `and_where_eq` would
// emit it on the left and Postgres rejects that as a syntax error.
sqlb.and_where("? = ANY(parent_hashes)".bind(&ph.0));
}
if let Some(it) = &lq.is_template {
sqlb.and_where_eq("is_template", it);
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: "3.0.3"
info:
version: 1.791.0
version: 1.792.1
title: Windmill API
contact:
@@ -78,6 +78,8 @@ struct ScriptMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
cache_ttl: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
cache_ignore_s3_path: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
dedicated_worker: Option<bool>,
#[serde(skip_serializing_if = "is_none_or_false")]
ws_error_handler_muted: Option<bool>,
@@ -826,6 +828,7 @@ pub(crate) async fn tarball_workspace(
concurrency_settings: script.runnable_settings.concurrency_settings,
debouncing_settings: script.runnable_settings.debouncing_settings,
cache_ttl: script.cache_ttl,
cache_ignore_s3_path: script.cache_ignore_s3_path,
dedicated_worker: script.dedicated_worker,
ws_error_handler_muted: script.ws_error_handler_muted,
priority: script.priority,
+1 -1
View File
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
export const VERSION = "v1.791.0";
export const VERSION = "v1.792.1";
export async function login(email: string, password: string): Promise<string> {
return await windmill.UserService.login({
+24 -1
View File
@@ -592,6 +592,7 @@ export async function handleFile(
ws_error_handler_muted: typed?.ws_error_handler_muted,
dedicated_worker: typed?.dedicated_worker,
cache_ttl: typed?.cache_ttl,
cache_ignore_s3_path: typed?.cache_ignore_s3_path,
concurrency_time_window_s: normConcurrencyTimeWindowS,
concurrent_limit: normConcurrentLimit,
deployment_message: message,
@@ -602,8 +603,14 @@ export async function handleFile(
concurrency_key: typed?.concurrency_key,
debounce_key: typed?.debounce_key,
debounce_delay_s: typed?.debounce_delay_s,
debounce_args_to_accumulate: typed?.debounce_args_to_accumulate,
max_total_debouncing_time: typed?.max_total_debouncing_time,
max_total_debounces_amount: typed?.max_total_debounces_amount,
codebase: await codebase?.getDigest(forceTar),
timeout: nonePositiveInt(typed?.timeout),
// 0 means "delete immediately after completion", so it must survive as 0
// rather than being folded into "unset" the way the positive-only settings are.
delete_after_secs: typed?.delete_after_secs,
on_behalf_of_email: typed?.on_behalf_of_email,
envs: typed?.envs,
modules: modules,
@@ -635,6 +642,12 @@ export async function handleFile(
(typed.description === remote.description &&
typed.summary === remote.summary &&
typed.kind == remote.kind &&
// A `.ts` file changes language when defaultTs flips, content untouched.
// bun and bunnative share that extension, so the inferred language is always
// bun; the server derives bunnative back from the `//native` annotation in
// the content, which is compared above.
language ==
(remote.language === "bunnative" ? "bun" : remote.language) &&
!remote.archived &&
(Array.isArray(remote?.lock)
? remote?.lock?.join("\n")
@@ -646,6 +659,8 @@ export async function handleFile(
remote.ws_error_handler_muted &&
typed.dedicated_worker == remote.dedicated_worker &&
typed.cache_ttl == remote.cache_ttl &&
Boolean(typed.cache_ignore_s3_path) ==
Boolean(remote.cache_ignore_s3_path) &&
normConcurrencyTimeWindowS ==
normalizeConcurrency(
remote.concurrent_limit,
@@ -659,15 +674,23 @@ export async function handleFile(
Boolean(remote.visible_to_runner_only) &&
Boolean(typed.has_preprocessor) ==
Boolean(remote.has_preprocessor) &&
typed.priority == Boolean(remote.priority) &&
typed.priority == remote.priority &&
nonePositiveInt(typed.timeout) == nonePositiveInt(remote.timeout) &&
typed.delete_after_secs == remote.delete_after_secs &&
//@ts-ignore
typed.concurrency_key == remote["concurrency_key"] &&
typed.debounce_key == remote["debounce_key"] &&
typed.debounce_delay_s == remote["debounce_delay_s"] &&
deepEqual(
typed.debounce_args_to_accumulate ?? null,
remote.debounce_args_to_accumulate ?? null
) &&
typed.max_total_debouncing_time == remote.max_total_debouncing_time &&
typed.max_total_debounces_amount == remote.max_total_debounces_amount &&
typed.codebase == remote.codebase &&
(hasOnBehalfOf ? true : typed.on_behalf_of_email == remote.on_behalf_of_email) &&
deepEqual(typed.envs, remote.envs) &&
deepEqual(typed.labels ?? null, remote.labels ?? null) &&
deepEqual(modules ?? null, remote.modules ?? null))
) {
log.info(colors.green(`Script ${remotePath} is up to date`));
+1 -1
View File
@@ -10,4 +10,4 @@ export const WM_FORK_PREFIX = "wm-fork";
// (e.g. utils.ts) can read it without importing main.ts and creating a circular
// dependency (main → workspace → utils → main) that triggers a TDZ.
// Re-exported from main.ts for backwards compatibility.
export const VERSION = "1.791.0";
export const VERSION = "1.792.1";
+145
View File
@@ -0,0 +1,145 @@
/**
* `wmill script push` short-circuits when the local script already matches the
* remote. The comparison has to hold in both directions: an untouched script
* deploys nothing, and every field the push body carries (labels and the language
* inferred from defaultTs included) still counts as a change.
*/
import { expect, test } from "bun:test";
import { writeFile, readFile, mkdir } from "node:fs/promises";
import { withTestBackend } from "./test_backend.ts";
import { waitForDeploymentJobs } from "./new_commands_helpers.ts";
test("Integration: script push skips an unchanged script and deploys a changed one", async () => {
await withTestBackend(async (backend, tempDir) => {
const uniqueId = Date.now();
const scriptPath = `f/test/uptodate_${uniqueId}`;
const getScript = async () =>
await (
await backend.apiRequest!(
`/api/w/${backend.workspace}/scripts/get/p/${scriptPath}`,
)
).json();
const push = async () =>
await backend.runCLICommand(["script", "push", `${scriptPath}.ts`], tempDir);
const wmillYaml = (defaultTs: string) =>
`defaultTs: ${defaultTs}\nincludes:\n - "${scriptPath}**"\nexcludes: []\n`;
await mkdir(`${tempDir}/f/test`, { recursive: true });
await backend.apiRequest!(`/api/w/${backend.workspace}/folders/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "test" }),
});
const createResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/scripts/create`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
path: scriptPath,
content: `export async function main() {\n return "Hello world";\n}`,
summary: "Test up to date",
description: "",
language: "bun",
kind: "script",
schema: {
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: {},
required: [],
},
labels: ["l1"],
}),
},
);
expect(createResp.ok).toEqual(true);
await writeFile(`${tempDir}/wmill.yaml`, wmillYaml("bun"), "utf-8");
// The lock a deploy's dependency job writes is part of the comparison, so every
// pull has to happen after that job has landed or the skip races it.
await waitForDeploymentJobs(backend);
expect((await backend.runCLICommand(["sync", "pull", "--yes"], tempDir)).code).toEqual(0);
const hashBefore = (await getScript()).hash;
expect((await push()).stdout).toContain("is up to date");
expect((await getScript()).hash).toEqual(hashBefore);
const metadataPath = `${tempDir}/${scriptPath}.script.yaml`;
await writeFile(
metadataPath,
(await readFile(metadataPath, "utf-8")).replace("- l1", "- l2"),
"utf-8",
);
expect((await push()).stdout).not.toContain("is up to date");
expect((await getScript()).labels).toEqual(["l2"]);
// 2, not 0 or 1: those two are the values a truthiness comparison would also
// call equal, so they cannot pin that priority is compared by value.
await waitForDeploymentJobs(backend);
expect((await backend.runCLICommand(["sync", "pull", "--yes"], tempDir)).code).toEqual(0);
await writeFile(metadataPath, (await readFile(metadataPath, "utf-8")) + "priority: 2\n", "utf-8");
expect((await push()).stdout).not.toContain("is up to date");
expect((await getScript()).priority).toEqual(2);
await waitForDeploymentJobs(backend);
expect((await backend.runCLICommand(["sync", "pull", "--yes"], tempDir)).code).toEqual(0);
expect((await push()).stdout).toContain("is up to date");
await writeFile(`${tempDir}/wmill.yaml`, wmillYaml("deno"), "utf-8");
expect((await push()).stdout).not.toContain("is up to date");
expect((await getScript()).language).toEqual("deno");
});
});
test("Integration: an unchanged bunnative script is not redeployed", async () => {
await withTestBackend(async (backend, tempDir) => {
const uniqueId = Date.now();
const scriptPath = `f/test/native_${uniqueId}`;
await mkdir(`${tempDir}/f/test`, { recursive: true });
await backend.apiRequest!(`/api/w/${backend.workspace}/folders/create`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "test" }),
});
const createResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/scripts/create`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
path: scriptPath,
// The server stores this as bunnative: the language is derived from the
// annotation, and no file extension can express it back.
content: `//native\nexport async function main() {\n return "Hello world";\n}`,
summary: "Test bunnative",
description: "",
language: "bun",
kind: "script",
}),
},
);
expect(createResp.ok).toEqual(true);
await writeFile(
`${tempDir}/wmill.yaml`,
`defaultTs: bun\nincludes:\n - "${scriptPath}**"\nexcludes: []\n`,
"utf-8",
);
await waitForDeploymentJobs(backend);
expect((await backend.runCLICommand(["sync", "pull", "--yes"], tempDir)).code).toEqual(0);
const remote = await (
await backend.apiRequest!(
`/api/w/${backend.workspace}/scripts/get/p/${scriptPath}`,
)
).json();
expect(remote.language).toEqual("bunnative");
const result = await backend.runCLICommand(
["script", "push", `${scriptPath}.ts`],
tempDir,
);
expect(result.stdout).toContain("is up to date");
});
});
@@ -0,0 +1,126 @@
/**
* Runtime settings that live only in the script metadata file (the retention
* delay, the debouncing bounds, the cache s3-path flag) must survive a sync
* pull/push cycle. A field missing from the create_script body the CLI builds
* lands as NULL on the deployed version; one missing from its up-to-date
* comparison makes a change to it alone report as up to date and never deploy.
*/
import { expect, test } from "bun:test";
import { writeFile, readFile, mkdir } from "node:fs/promises";
import { withTestBackend } from "./test_backend.ts";
import { waitForDeploymentJobs } from "./new_commands_helpers.ts";
// The debounce bounds this PR also restores cannot be asserted here: a build without
// git tags reports a bare commit as its version, GIT_SEM_VERSION then falls back to
// 0.1.0, and every version-gated feature (debouncing wants 1.566.0) is refused. That is
// what CI builds, so a fixture that sets any debounce field fails at create.
const SETTINGS = {
delete_after_secs: 900,
cache_ignore_s3_path: true,
};
test("Integration: script runtime settings survive a sync pull/push cycle", async () => {
await withTestBackend(async (backend, tempDir) => {
const uniqueId = Date.now();
const scriptPath = `f/test/settings_${uniqueId}`;
const getScript = async () => {
const resp = await backend.apiRequest!(
`/api/w/${backend.workspace}/scripts/get/p/${scriptPath}`,
);
expect(resp.ok).toEqual(true);
return await resp.json();
};
await mkdir(`${tempDir}/f/test`, { recursive: true });
const folderResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/folders/create`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "test" }),
},
);
const folderStatus = `${folderResp.status} ${await folderResp.text()}`;
const createResp = await backend.apiRequest!(
`/api/w/${backend.workspace}/scripts/create`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
path: scriptPath,
content: `export async function main() {\n return "Hello world";\n}`,
summary: "Test runtime settings",
description: "",
language: "bun",
kind: "script",
schema: {
$schema: "https://json-schema.org/draft/2020-12/schema",
type: "object",
properties: {},
required: [],
},
...SETTINGS,
}),
},
);
if (!createResp.ok) {
throw new Error(
`scripts/create failed: ${createResp.status} ${await createResp.text()} ` +
`(folders/create: ${folderStatus})`,
);
}
await writeFile(
`${tempDir}/wmill.yaml`,
`defaultTs: bun\nincludes:\n - "${scriptPath}**"\nexcludes: []\n`,
"utf-8",
);
// The lock a deploy's dependency job writes is compared before the settings are,
// so a pull taken before that job lands makes the next push deploy over the lock
// instead of over the setting under test.
await waitForDeploymentJobs(backend);
const pullResult = await backend.runCLICommand(["sync", "pull", "--yes"], tempDir);
expect(pullResult.code).toEqual(0);
const metadataPath = `${tempDir}/${scriptPath}.script.yaml`;
const pulledMetadata = await readFile(metadataPath, "utf-8");
for (const key of Object.keys(SETTINGS)) {
expect(pulledMetadata).toContain(key);
}
// A content-only edit must carry the settings through to the new version.
const scriptFilePath = `${tempDir}/${scriptPath}.ts`;
const originalContent = await readFile(scriptFilePath, "utf-8");
await writeFile(
scriptFilePath,
originalContent.replace("Hello world", "Hello world modified"),
"utf-8",
);
expect((await backend.runCLICommand(["sync", "push", "--yes"], tempDir)).code).toEqual(0);
const afterContentPush = await getScript();
expect(afterContentPush.content).toContain("Hello world modified");
for (const [key, value] of Object.entries(SETTINGS)) {
expect(afterContentPush[key]).toEqual(value);
}
// A settings-only edit must reach the remote rather than be skipped as up to
// date. 0 is "delete immediately after completion", not "unset".
await waitForDeploymentJobs(backend);
expect((await backend.runCLICommand(["sync", "pull", "--yes"], tempDir)).code).toEqual(0);
await writeFile(
metadataPath,
(await readFile(metadataPath, "utf-8")).replace(
`delete_after_secs: ${SETTINGS.delete_after_secs}`,
"delete_after_secs: 0",
),
"utf-8",
);
expect((await backend.runCLICommand(["sync", "push", "--yes"], tempDir)).code).toEqual(0);
expect((await getScript()).delete_after_secs).toEqual(0);
});
});
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@windmill-labs/components",
"version": "1.791.0",
"version": "1.792.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@windmill-labs/components",
"version": "1.791.0",
"version": "1.792.1",
"hasInstallScript": true,
"license": "AGPL-3.0",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@windmill-labs/components",
"version": "1.791.0",
"version": "1.792.1",
"scripts": {
"dev": "vite dev",
"dev:ui-builder": "mv static/ui_builder static/ui_builder.dev-disabled 2>/dev/null || true ; trap 'mv static/ui_builder.dev-disabled static/ui_builder 2>/dev/null || true' EXIT ; vite dev",
@@ -7,6 +7,8 @@
import AppConnectInner from './AppConnectInner.svelte'
import DarkModeObserver from './DarkModeObserver.svelte'
import IconedResourceType from './IconedResourceType.svelte'
import { addResourceTitle } from './resourceTypeDisplay'
interface Props {
expressOAuthSetup?: boolean
@@ -59,12 +61,17 @@
{disableChatOffset}
>
<DrawerContent
title="Add a resource"
title={addResourceTitle(step > 1 ? resourceType : undefined)}
id="add-resource-drawer"
on:close={drawer?.closeDrawer}
tooltip="Resources represent connections to third party systems. Learn more on how to integrate external APIs."
documentationLink="https://www.windmill.dev/docs/integrations/integrations_on_windmill"
>
{#snippet titleExtra()}
{#if step > 1 && resourceType}
<IconedResourceType name={resourceType} silent width="20px" height="20px" />
{/if}
{/snippet}
<AppConnectInner
bind:this={appConnectInner}
bind:step
@@ -4,6 +4,7 @@
import { userStore, workspaceStore } from '$lib/stores'
import LabelsInput from './LabelsInput.svelte'
import IconedResourceType from './IconedResourceType.svelte'
import { resourceTypeSearchText, sortResourceTypesByMatch } from './resourceTypeDisplay'
import {
OauthService,
ResourceService,
@@ -22,7 +23,6 @@
import WhitelistIp from './WhitelistIp.svelte'
import { sendUserToast } from '$lib/toast'
import OauthScopes from './OauthScopes.svelte'
import Markdown from 'svelte-exmarkdown'
import autosize from '$lib/autosize'
import { base } from '$lib/base'
import Required from './Required.svelte'
@@ -35,6 +35,7 @@
import { sameTopDomainOrigin } from '$lib/cookies'
import SyncResourceTypes from './SyncResourceTypes.svelte'
import Label from './Label.svelte'
import ResourcePathHint from './ResourcePathHint.svelte'
interface Props {
step?: number
@@ -98,6 +99,7 @@
let connectClient: string = $state('')
let connectsManual: { key: string; img?: string; instructions: string[] }[] | undefined =
$state(undefined)
let resourceTypeDescriptions: Record<string, string> = $state({})
let args: any = $state({})
let renderDescription = $state(true)
@@ -396,6 +398,20 @@
workspace: effectiveWorkspace
})
// Descriptions only feed search, so they are fetched off the critical path and
// allowed to fail: `resources/type/list` is not on the public app domain's route
// allow-list (`listnames` is), and it carries every type's full schema. Awaiting it
// would hold the list behind a request nothing on screen needs -- in a published
// app, behind one that is guaranteed to 403. resourceTypeDescriptions feeds a
// $derived, so search re-ranks when they land.
ResourceService.listResourceType({ workspace: effectiveWorkspace })
.then((types) => {
resourceTypeDescriptions = Object.fromEntries(
types.filter((t) => t.description).map((t) => [t.name, t.description!])
)
})
.catch(() => {})
// "Others" lists every resource type — including instance-configured OAuth
// providers — so any of them can also be connected with the user's own
// credentials or manually, not only via the shared instance setup (same as
@@ -870,6 +886,22 @@
let filteredConnects: { key: string }[] = $state([])
let filteredConnectsManual: { key: string; img?: string; instructions: string[] }[] = $state([])
// uFuzzy scores the name and the description as one string, so searching "google" ranks
// every type whose description mentions Google alongside the ones named after it. Re-sort
// on which field matched, keeping uFuzzy's order within a tier.
const rank = (items: { key: string }[] | undefined) =>
items &&
sortResourceTypesByMatch(
items,
filter,
(x) => x.key,
(x) => resourceTypeDescriptions[x.key]
)
let rankedConnects = $derived(rank(filteredConnects))
let rankedConnectsManual = $derived(
rank(filteredConnectsManual) as typeof filteredConnectsManual | undefined
)
let editScopes = $state(false)
</script>
@@ -882,13 +914,13 @@
}))
: undefined}
bind:filteredItems={filteredConnects}
f={(x) => x.key}
f={(x) => resourceTypeSearchText(x.key, resourceTypeDescriptions[x.key])}
/>
<SearchItems
{filter}
items={connectsManual}
bind:filteredItems={filteredConnectsManual}
f={(x) => x.key}
f={(x) => resourceTypeSearchText(x.key, resourceTypeDescriptions[x.key])}
/>
{#if step == 1}
<div class="pb-2 my-1">
@@ -904,8 +936,8 @@
<h2 class="mb-4 text-sm font-semibold text-emphasis">Instance-configured OAuth APIs</h2>
<div class="grid sm:grid-cols-2 md:grid-cols-3 gap-x-2 gap-y-1 items-center">
{#if filteredConnects}
{#each filteredConnects as { key }}
{#if rankedConnects}
{#each rankedConnects as { key }}
<Button
unifiedSize="md"
variant="default"
@@ -945,8 +977,8 @@
{/if}
<div class="grid sm:grid-cols-2 md:grid-cols-3 gap-x-2 gap-y-1 items-center mb-2">
{#if filteredConnectsManual}
{#each filteredConnectsManual as { key }}
{#if rankedConnectsManual}
{#each rankedConnectsManual as { key }}
{#if nativeLanguagesCategory.includes(key)}
<Button
unifiedSize="md"
@@ -959,8 +991,8 @@
{/if}
{/each}
{/if}
{#if filteredConnectsManual}
{#each filteredConnectsManual as { key }}
{#if rankedConnectsManual}
{#each rankedConnectsManual as { key }}
{#if !nativeLanguagesCategory.includes(key)}
<!-- Exclude specific items -->
<Button
@@ -993,7 +1025,11 @@
</div>
{:else if step == 2 && manual}
<div class="flex flex-col gap-4">
{#if !emptyString(resourceTypeInfo?.description)}
<GfmMarkdown md={urlize(resourceTypeInfo?.description ?? '', 'md')} prose="sm" noPadding />
{/if}
<Label label="Path">
<ResourcePathHint />
<Path
bind:error={pathError}
bind:path
@@ -1013,9 +1049,9 @@
{/if}
{#if apiTokenApps[resourceType]}
<h2 class="mt-4 mb-2">Instructions</h2>
<div class="pl-10">
<ol class="list-decimal">
<div class="flex flex-col gap-2">
<h2 class="text-sm font-semibold text-emphasis">Instructions</h2>
<ol class="list-decimal pl-5 text-xs text-primary flex flex-col gap-1">
{#each apiTokenApps[resourceType].instructions as step}
<li>
{@html step}
@@ -1032,15 +1068,6 @@
/>
</div>
{/if}
{:else if !emptyString(resourceTypeInfo?.description)}
<label class="flex flex-col gap-1">
<span class="text-sm font-semibold text-emphasis">
{resourceTypeInfo?.name} description
</span>
<div class="text-xs text-primary font-normal">
<Markdown md={urlize(resourceTypeInfo?.description ?? '', 'md')} />
</div>
</label>
{/if}
{#if resourceType == 'postgresql' || resourceType == 'mysql' || resourceType == 'mongodb'}
<WhitelistIp />
@@ -1068,7 +1095,7 @@
{:else if description == undefined || description == ''}
<div class="text-xs text-primary font-normal">No description provided</div>
{:else}
<GfmMarkdown md={description} />
<GfmMarkdown md={description} prose="sm" />
{/if}
</div>
@@ -1128,12 +1155,11 @@
</div>
{#if resourceTypeInfo?.description}
<div class="flex flex-col gap-1">
<h3 class="text-sm font-semibold text-emphasis">Description</h3>
<div class="text-xs text-primary font-normal">
<Markdown md={urlize(resourceTypeInfo?.description ?? '', 'md')} />
</div>
</div>
<GfmMarkdown
md={urlize(resourceTypeInfo?.description ?? '', 'md')}
prose="sm"
noPadding
/>
{/if}
<LabelsInput bind:labels class="-mt-5" />
+17 -5
View File
@@ -10,7 +10,7 @@
import { buildWsUrl } from '$lib/wsUrl'
import { sendUserToast } from '$lib/toast'
import { createEventDispatcher, onDestroy, onMount, tick, untrack } from 'svelte'
import { createEventDispatcher, getContext, onDestroy, onMount, tick, untrack } from 'svelte'
// import libStdContent from '$lib/es6.d.ts.txt?raw'
// import domContent from '$lib/dom.d.ts.txt?raw'
@@ -110,7 +110,8 @@
import FakeMonacoPlaceHolder from './FakeMonacoPlaceHolder.svelte'
import { editorPositionMap } from '$lib/utils'
import { extToLang, langToExt } from '$lib/editorLangUtils'
import { aiChatManager } from './copilot/chat/AIChatManager.svelte'
import { aiChatManager, type AIChatManager } from './copilot/chat/AIChatManager.svelte'
import { chatState } from './copilot/chat/sharedChatState.svelte'
import type { Selection } from 'monaco-editor'
import { canHavePreprocessor, getPreprocessorModuleCode } from '$lib/script_helpers'
import { setMonacoTypescriptOptions } from './monacoLanguagesOptions'
@@ -264,6 +265,11 @@
let disposeMethod: (() => void) | undefined
const absolutePathExtraLibs = new Map<string, { dispose: () => void }>()
const dispatch = createEventDispatcher()
// Set by the sessions pane; undefined everywhere else. ⌘L targets it so the
// shortcut reaches the chat the user is actually looking at. Read directly
// rather than via getAiChatManager(), which collapses "no session" into the
// singleton — the distinction is what tells ⌘L whether a pane exists to open.
const sessionScopedChatManager = getContext<AIChatManager | undefined>('aiChatManager')
// let graphqlService: MonacoGraphQLAPI | undefined = undefined
let dbSchema: DBSchema | undefined = $state(undefined)
@@ -1695,16 +1701,22 @@
selection &&
(selection.startLineNumber !== selection.endLineNumber ||
selection.startColumn !== selection.endColumn)
// Target whichever chat is actually on screen: the session's own in a
// session pane, else the docked one. With sessions on outside a pane
// there is neither, and both branches below would be silent no-ops.
const chat = sessionScopedChatManager ?? aiChatManager
if (!sessionScopedChatManager && !chatState.dockedChatAvailable) return
if (hasSelection && selectedLines) {
aiChatManager.addSelectedLinesToContext(
chat.addSelectedLinesToContext(
selectedLines,
selection.startLineNumber,
selection.endLineNumber,
moduleId
)
} else {
aiChatManager.toggleOpen()
aiChatManager.focusInput()
// A session chat is always visible — only the docked pane toggles.
if (!sessionScopedChatManager) chat.toggleOpen()
chat.focusInput()
}
})
@@ -1,13 +1,17 @@
<script lang="ts">
import { Markdown } from 'svelte-exmarkdown'
import { markdownPlugins as plugins } from './markdownPlugins'
import { markdownProse, type MarkdownProseSize } from './markdownProse'
import { isOfflineReplay } from './recording/offlineReplay.svelte'
interface Props {
md: string
noPadding?: boolean
/** Shared prose stack to render with. Omitted keeps the legacy `prose-xs`,
* which the flow-graph notes are laid out against. */
prose?: MarkdownProseSize
}
let { md, noPadding }: Props = $props()
let { md, noPadding, prose }: Props = $props()
// Rendering markdown turns `![](url)` into a real `<img>`, i.e. a request. On the
// public replay page the source is a recording from an arbitrary origin and the
@@ -17,7 +21,7 @@
let asPlainText = $derived(isOfflineReplay())
</script>
<div class="!prose-xs {noPadding ? '' : 'pgap'}">
<div class="{prose ? markdownProse[prose] : '!prose-xs'} {noPadding ? '' : 'pgap'}">
{#if asPlainText}
<p class="whitespace-pre-wrap">{md}</p>
{:else}
@@ -8,6 +8,8 @@
import type { AppViewerContext } from './apps/types'
import { sendUserToast } from '$lib/toast'
import Select from './select/Select.svelte'
import IconedResourceType from './IconedResourceType.svelte'
import { addResourceTitle } from './resourceTypeDisplay'
interface Props {
value: string | undefined
@@ -103,11 +105,16 @@
{:else}
<Drawer bind:this={drawer} size="800px">
<DrawerContent
title="Add a resource"
title={addResourceTitle(resourceType)}
on:close={drawer.closeDrawer}
tooltip="Resources represent connections to third party systems. Learn more on how to integrate external APIs."
documentationLink="https://www.windmill.dev/docs/integrations/integrations_on_windmill"
>
{#snippet titleExtra()}
{#if resourceType}
<IconedResourceType name={resourceType} silent width="20px" height="20px" />
{/if}
{/snippet}
{#await import('./AppConnectLightweightResourcePicker.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
@@ -85,8 +85,12 @@
bind:this={outputPickerInner}
>
{#snippet copilot_fix()}
{#if lang && editor && diffEditor && stepsInputArgs.getStepArgs(mod.id) && selectedJob?.type === 'CompletedJob' && !selectedJob.success && getStringError(selectedJob.result)}
<ScriptFix {lang} />
{@const stepError =
selectedJob?.type === 'CompletedJob' && !selectedJob.success
? getStringError(selectedJob.result)
: undefined}
{#if lang && editor && diffEditor && stepsInputArgs.getStepArgs(mod.id) && stepError}
<ScriptFix {lang} error={stepError} jobId={selectedJob?.id} moduleId={mod.id} />
{/if}
{/snippet}
</OutputPickerInner>
@@ -16,6 +16,8 @@
} from './sessions/pageDrawerSession'
import { RESOURCES_PATH } from './sessions/previewPaths'
import ResourceVersionHistory from './ResourceVersionHistory.svelte'
import IconedResourceType from './IconedResourceType.svelte'
import { addResourceTitle } from './resourceTypeDisplay'
let {
workspace = undefined,
@@ -90,10 +92,15 @@
on:close={() => clearPageDrawerAnchor(RESOURCES_PATH)}
>
<DrawerContent
title={mode == 'edit' ? 'Edit ' + path : 'Add a resource'}
title={mode == 'edit' ? 'Edit ' + path : addResourceTitle(resource_type)}
bannerReserved={mode == 'edit'}
on:close={drawer?.closeDrawer}
>
{#snippet titleExtra()}
{#if mode == 'new' && resource_type}
<IconedResourceType name={resource_type} silent width="20px" height="20px" />
{/if}
{/snippet}
{#await import('./ResourceEditor.svelte')}
<Loader2 class="animate-spin" />
{:then Module}
@@ -15,7 +15,6 @@
import Toggle from './Toggle.svelte'
import TestConnection from './TestConnection.svelte'
import { Pen } from 'lucide-svelte'
import Markdown from 'svelte-exmarkdown'
import autosize from '$lib/autosize'
import GfmMarkdown from './GfmMarkdown.svelte'
import TestTriggerConnection from './triggers/TestTriggerConnection.svelte'
@@ -24,6 +23,7 @@
import ResourceGen from './copilot/ResourceGen.svelte'
import SyncResourceTypes from './SyncResourceTypes.svelte'
import Label from './Label.svelte'
import ResourcePathHint from './ResourcePathHint.svelte'
interface Props {
path: string
@@ -142,6 +142,10 @@
})
</script>
{#if !emptyString(resourceTypeInfo?.description)}
<GfmMarkdown md={urlize(resourceTypeInfo?.description ?? '', 'md')} prose="sm" noPadding />
{/if}
{#if !hidePath}
<div>
{#if !can_write}
@@ -152,6 +156,7 @@
</div>
{/if}
<Label label="Path">
<ResourcePathHint />
<Path
disabled={initialPath != '' && !isOwner(initialPath, $userStore, ws)}
bind:path
@@ -174,15 +179,6 @@
</Label>
{/if}
{#if !emptyString(resourceTypeInfo?.description)}
<div class="flex flex-col gap-1">
<h4 class="text-xs text-emphasis font-semibold">{resourceTypeInfo?.name} description</h4>
<div class="text-xs text-primary font-normal">
<Markdown md={urlize(resourceTypeInfo?.description ?? '', 'md')} />
</div>
</div>
{/if}
<div class="flex flex-col gap-1">
<h4 class="inline-flex items-center gap-2 text-xs text-emphasis font-semibold"
>Resource description <Required required={false} />
@@ -210,9 +206,7 @@
{:else if description == undefined || description == ''}
<div class="text-xs text-secondary font-normal">No description provided</div>
{:else}
<div class="text-xs text-primary font-normal">
<GfmMarkdown md={description} noPadding />
</div>
<GfmMarkdown md={description} prose="sm" noPadding />
{/if}
</div>
@@ -0,0 +1,7 @@
<!-- Shown above the Path input wherever a resource is created: the resource form and the
connect drawer's last step. One component so the two screens cannot drift apart. -->
<div class="text-xs text-secondary font-normal mb-1">
The path sets who can access this resource: a <code>u/</code> path is private to that user, an
<code>f/</code> path follows the folder's permissions — read access lets people use the resource, write
access lets them edit it.
</div>
@@ -9,6 +9,7 @@
import Tooltip from './Tooltip.svelte'
import Badge from './common/badge/Badge.svelte'
import { untrack } from 'svelte'
import { resourceTypeSearchText, sortResourceTypesByMatch } from './resourceTypeDisplay'
interface Props {
value: string | undefined
notPickable?: boolean
@@ -17,10 +18,15 @@
let { value = $bindable(), notPickable = false, nonePickable = false }: Props = $props()
let resources: string[] = $state([])
let resources: { name: string; description?: string; searchText: string }[] = $state([])
async function loadResources() {
resources = await ResourceService.listResourceTypeNames({ workspace: $workspaceStore! })
const types = await ResourceService.listResourceType({ workspace: $workspaceStore! })
resources = types.map((t) => ({
name: t.name,
description: t.description,
searchText: resourceTypeSearchText(t.name, t.description)
}))
}
const dispatch = createEventDispatcher()
@@ -40,7 +46,12 @@
let search: string = $state('')
let filteredResources = $derived(
resources.filter((r) => r.toLowerCase().includes(search.toLowerCase()))
sortResourceTypesByMatch(
resources.filter((r) => r.searchText.toLowerCase().includes(search.trim().toLowerCase())),
search,
(r) => r.name,
(r) => r.description
)
)
</script>
@@ -78,18 +89,18 @@
</Button>
{/if}
{#each filteredResources as r}
{@const isPicked = value === r}
{@const isPicked = value === r.name}
<Button
size="sm"
variant="default"
selected={isPicked}
disabled={notPickable}
on:click={() => {
onClick(r)
onClick(r.name)
close()
}}
>
<IconedResourceType name={r} after={true} width="20px" height="20px" />
<IconedResourceType name={r.name} after={true} width="20px" height="20px" />
</Button>
{/each}
@@ -95,6 +95,7 @@
import OpenInSessionButton, {
type OpenInSessionSource
} from './sessions/OpenInSessionButton.svelte'
import { setOpenInSessionHandoff } from './sessions/openInSessionContext'
// Forward-looking hook for the upcoming session-pane feature: that PR will
// `setContext('aiChatManager', ...)` from the session wrapper so this editor
@@ -278,6 +279,14 @@
let opWs = $derived(workspaceOverride ?? $workspaceStore)
// Publish this editor's hand-off for AI entry points below it (the preview
// panel's "AI Fix"), withheld under `disableAi` so an embed that turned AI off
// gets no entry point that navigates its host to /sessions. Shadows an
// ancestor's hand-off deliberately: ScriptEditorDrawer mounts this without a
// `sessionOpen`, and falling through to FlowBuilder's would answer "fix this
// script" by opening the flow and abandoning the drawer's unsaved content.
setOpenInSessionHandoff({ source: () => (disableAi ? undefined : sessionOpen) })
$effect(() => {
onTestStateChange?.(testIsLoading)
})
@@ -116,15 +116,15 @@
{:else if effectiveKind === 'postgres'}
<Database {size} class="text-gray-400" />
{:else if effectiveKind === 'kafka'}
<KafkaIcon {size} class="text-gray-400" />
<KafkaIcon {size} />
{:else if effectiveKind === 'nats'}
<NatsIcon {size} class="text-gray-400" />
<NatsIcon {size} />
{:else if effectiveKind === 'mqtt'}
<MqttIcon {size} class="text-gray-400" />
<MqttIcon {size} />
{:else if effectiveKind === 'amqp'}
<AmqpIcon {size} class="text-gray-400" />
<AmqpIcon {size} />
{:else if effectiveKind === 'sqs'}
<AwsIcon {size} class="text-gray-400" />
<AwsIcon {size} />
{:else if effectiveKind === 'gcp'}
<GoogleCloudIcon {size} />
{:else if effectiveKind === 'azure'}
@@ -1,27 +1,67 @@
<script lang="ts">
import { Button } from '$lib/components/common'
import { Pencil } from 'lucide-svelte'
import { Pencil, WandSparkles } from 'lucide-svelte'
import { aiChatManager } from './chat/AIChatManager.svelte'
import AskAiButton from './AskAiButton.svelte'
import OpenInSessionButton from '$lib/components/sessions/OpenInSessionButton.svelte'
import { AIBtnClasses } from './chat/AIButtonStyle'
import { workspaceStore } from '$lib/stores'
interface Props {
onEditInstructions: () => void
instructions: string
runnableType: 'script' | 'flow'
path: string | undefined
}
const { onEditInstructions, instructions, runnableType }: Props = $props()
const { onEditInstructions, instructions, runnableType, path }: Props = $props()
async function fillFormWithAI() {
aiChatManager.openChat()
aiChatManager.askAi(`Analyze the ${runnableType} form on this page and fill the inputs for me`)
}
// A session cannot reach this page's form (the preview is a separate editor,
// and form filling drives the DOM through NAVIGATOR mode), so the hand-off
// asks it to run the item instead. Naming the DEPLOYED version matters: the
// test_run_* tools prefer drafts, which is not what this page runs.
const sessionSource = $derived(
path
? {
target: { kind: runnableType, path } as const,
workspaceId: $workspaceStore ?? undefined,
seedPrompt:
`Run the deployed ${runnableType} \`${path}\` for me. Pick sensible inputs, ` +
`tell me what you chose, then run it.` +
(instructions ? `\n\nHow to choose the inputs:\n${instructions}` : '')
}
: undefined
)
</script>
<div class="my-3 p-3 bg-surface-secondary rounded-md relative flex flex-col gap-3">
<div class="flex flex-row gap-2 justify-between items-center">
<h3 class="text-sm font-medium">Fill the inputs with AI</h3>
<AskAiButton label="Fill with AI" onClick={fillFormWithAI} />
<!-- Heading stays neutral because the two branches do different things: the
hand-off runs the item, the legacy path fills the form. Each button
names its own action. A plain Button rather than AskAiButton, whose own
session branch would fire here too and open an empty session. -->
<h3 class="text-sm font-medium">AI can help with these inputs</h3>
<OpenInSessionButton
source={sessionSource}
label="Run in AI session"
tooltip="Open an AI session that picks inputs and runs this"
btnProps={{ iconOnly: false, startIcon: { icon: WandSparkles } }}
>
{#snippet fallback()}
<Button
unifiedSize="md"
startIcon={{ icon: WandSparkles }}
btnClasses={AIBtnClasses('default')}
on:click={fillFormWithAI}
>
Fill with AI
</Button>
{/snippet}
</OpenInSessionButton>
</div>
<div class="flex flex-row gap-2 items-center">
<p class="text-sm text-primary">
@@ -3,6 +3,9 @@
import { WandSparkles } from 'lucide-svelte'
import { aiChatManager } from './chat/AIChatManager.svelte'
import { AIBtnClasses } from './chat/AIButtonStyle'
import { prefersSessionHandoff } from './chat/global/gate'
import { startSessionWithPrompt } from '$lib/components/sessions/sessionSwitch.svelte'
import { userStore } from '$lib/stores'
interface Props {
label?: string
initialInput?: string
@@ -11,7 +14,20 @@
const { label, initialInput, onClick: onClickProp }: Props = $props()
// The label stays short ("Ask AI") for the search bar's inline row; the hover
// text is where "new AI session" fits.
const handsOffToSession = $derived(prefersSessionHandoff($userStore?.operator))
export function onClick() {
// No item to preview here — this carries a question, not a target — so the
// hand-off opens a bare session on the question alone.
if (handsOffToSession) {
onClickProp?.()
// Sent on arrival, matching the legacy path below (askAi sends straight
// away): the text is the question the user already typed.
void startSessionWithPrompt(initialInput ?? '', { autoSend: true })
return
}
aiChatManager.openChat()
if (initialInput) {
aiChatManager.askAi(initialInput, {
@@ -30,6 +46,7 @@
}}
unifiedSize="md"
btnClasses={AIBtnClasses('default')}
title={handsOffToSession ? 'Ask this in a new AI session' : 'Ask this in the AI chat'}
on:click={onClick}
>
{label}
@@ -7,63 +7,123 @@
import Popover from '$lib/components/meltComponents/Popover.svelte'
import { autoPlacement } from '@floating-ui/core'
import { WandSparkles } from 'lucide-svelte'
import { aiChatManager } from './chat/AIChatManager.svelte'
import { aiChatManager, type AIChatManager } from './chat/AIChatManager.svelte'
import { copilotInfo } from '$lib/aiStore'
import OpenInSessionButton from '$lib/components/sessions/OpenInSessionButton.svelte'
import { getOpenInSessionHandoff } from '$lib/components/sessions/openInSessionContext'
import { AIBtnClasses } from './chat/AIButtonStyle'
import { getContext } from 'svelte'
let {
lang
lang,
error,
jobId,
moduleId
}: {
lang: SupportedLanguage
/** The failing run's error, used when there is no job to point at. */
error?: string
/** The failing run's job id. Preferred over `error`: the chat reads the
* run itself with `get_job_logs`, which gives it the logs rather than
* just the thrown value, and keeps the composer readable. */
jobId?: string
/** Set when this sits in a flow step's preview, so the session opens on
* that step rather than the flow root. */
moduleId?: string
} = $props()
// The enclosing editor's "Open in AI session" hand-off (ScriptEditor for a
// standalone script, FlowBuilder for a step).
const handoff = getOpenInSessionHandoff()
const seedPrompt = $derived.by(() => {
const what = moduleId ? `step \`${moduleId}\`` : 'this script'
if (jobId) {
return `The last test run of ${what} failed (job \`${jobId}\`). Read its logs, then fix the code.`
}
// No job to read: the error text has to travel with the request.
return error
? `Fix this error in ${what}:\n\n\`\`\`\n${error}\n\`\`\``
: `Fix the error from the last run of ${what}.`
})
const sessionSource = $derived.by(() => {
const source = handoff?.source({ moduleId })
return source ? { ...source, seedPrompt, autoSend: true } : undefined
})
// Inside a session pane the chat is already beside this panel, so there is
// nothing to hand off to: send into that chat instead. OpenInSessionButton
// renders nothing there, which would otherwise leave the sessions population
// with no fix affordance at all.
const sessionScopedManager = getContext<AIChatManager | undefined>('aiChatManager')
</script>
{#if SUPPORTED_LANGUAGES.has(lang)}
<Popover
floatingConfig={{
middleware: [
autoPlacement({
allowedPlacements: ['bottom-end', 'top-end']
})
]
}}
displayArrow={true}
>
{#snippet trigger()}
<div class="flex flex-row">
<Button
title="Fix code"
size="xs"
color="light"
spacingSize="xs2"
startIcon={{ icon: WandSparkles }}
on:click={() => {
if ($copilotInfo.enabled) {
aiChatManager.fix()
}
}}
btnClasses="text-ai bg-violet-100 dark:bg-gray-700 min-w-[84px]"
propagateEvent={!$copilotInfo.enabled}
>
AI Fix
</Button>
</div>
{#if sessionScopedManager}
<Button
title="Fix the failing run in this chat"
size="xs"
color="light"
spacingSize="xs2"
startIcon={{ icon: WandSparkles }}
on:click={() => sessionScopedManager.sendOrQueue(seedPrompt)}
btnClasses={AIBtnClasses('default')}
>
AI Fix
</Button>
{:else}
<OpenInSessionButton
source={sessionSource}
btnClasses={AIBtnClasses('default')}
label="Fix in AI session"
tooltip="Open an AI session on this item and fix the failing run"
btnProps={{ iconOnly: false, startIcon: { icon: WandSparkles } }}
>
{#snippet fallback()}
<Popover
floatingConfig={{
middleware: [
autoPlacement({
allowedPlacements: ['bottom-end', 'top-end']
})
]
}}
displayArrow={true}
>
{#snippet trigger()}
<div class="flex flex-row">
<Button
title="Fix code"
size="xs"
color="light"
spacingSize="xs2"
startIcon={{ icon: WandSparkles }}
on:click={() => {
if ($copilotInfo.enabled) {
aiChatManager.fix()
}
}}
btnClasses="text-ai bg-violet-100 dark:bg-gray-700 min-w-[84px]"
propagateEvent={!$copilotInfo.enabled}
>
AI Fix
</Button>
</div>
{/snippet}
{#snippet content()}
<div class="p-4">
<div class="w-80">
<p class="text-sm"
>Enable Windmill AI in the <a
class="inline-flex flex-row items-center gap-1"
href="{base}/workspace_settings?tab=ai"
target="_blank">workspace settings</a
></p
></div
>
</div>
{/snippet}
</Popover>
{/snippet}
{#snippet content()}
<div class="p-4">
<div class="w-80">
<p class="text-sm"
>Enable Windmill AI in the <a
class="inline-flex flex-row items-center gap-1"
href="{base}/workspace_settings?tab=ai"
target="_blank">workspace settings</a
></p
></div
>
</div>
{/snippet}
</Popover>
</OpenInSessionButton>
{/if}
{/if}
@@ -12,7 +12,8 @@
togglePanel,
btnClasses,
btnProps,
label = 'Open in AI session'
label = 'Open in AI session',
tooltip
}: {
togglePanel: () => void
btnClasses?: string
@@ -21,13 +22,19 @@
btnProps?: ComponentProps<typeof Button>
/** Tooltip + accessible text of the icon-only button. */
label?: string
/** Hover text, when the label alone doesn't say where the button leads.
* A host that renamed the button ("AI Fix") uses this to keep "in a new
* AI session" discoverable. Defaults to `label`. */
tooltip?: string
} = $props()
const hoverText = $derived(tooltip ?? label)
</script>
{#if $copilotInfo.enabled}
<DarkPopover>
{#snippet text()}
{label}
{hoverText}
{/snippet}
{@render button({ onPress: () => togglePanel() })}
</DarkPopover>
@@ -166,9 +166,21 @@
files: initialFiles ?? []
}))
)
// Report edits, never the mount-time value. The first run carries whatever the
// composer was constructed with, which is not something the user did: a
// composer deliberately mounted empty (its text is already in flight, or
// belongs to a session this view is only keeping warm) would otherwise report
// '' and overwrite the very prompt it was withholding.
let draftReported = false
$effect(() => {
const text = draft.text
untrack(() => onDraftChange?.(text))
untrack(() => {
if (!draftReported) {
draftReported = true
return
}
onDraftChange?.(text)
})
})
// Images being decoded right now. Holds off sending so a message can never go
// out without an attachment the user already dropped, and reserves cap slots
@@ -1774,6 +1774,22 @@ export class AIChatManager {
}
}
/** Send `text` as a turn, or queue it when one is already streaming. Callers
* that send programmatically (an editor button, an arriving hand-off) must go
* through this rather than `sendRequest`: a second concurrent loop shares this
* manager's abort controller and transcript, so the two interleave and Stop
* halts only one. It is the rule the composer already follows.
*
* Gated on `sendInFlight` as well as `loading`: `loading` only rises after a
* send's attachment upkeep, so between the two a click would slip past. */
sendOrQueue(text: string) {
if (this.loading || this.sendInFlight) {
this.queueMessage(text)
return
}
void this.sendRequest({ instructions: text })
}
/** Remove the queued message and put it back into the input, images included. */
dequeueMessage() {
if (!this.#hasQueuedMessage()) {
@@ -2609,6 +2625,14 @@ export class AIChatManager {
}
sendRequest = async (options: Parameters<typeof this.sendRequestImpl>[0] = {}) => {
// A turn with nowhere to render still streams, spends tokens and applies
// tool calls — entirely off-screen. Refuse instead. `sendInlineRequest` is
// exempt: the ⌘K widget renders its own composer inside Monaco.
if (!this.isSessionChat && !chatState.dockedChatAvailable) {
console.error('sendRequest called with no chat UI mounted; dropping the turn')
sendUserToast('This action needs the AI chat. Start an AI session to continue.', true)
return
}
this.#sendsInFlight++
try {
return await this.sendRequestImpl(options)
@@ -6,6 +6,7 @@ import type { ReviewChangesOpts } from './monaco-adapter'
import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions.mjs'
import type { AttachedImage } from './imageUtils'
import { AIChatManager, AIMode, AIAutonomyMode } from './AIChatManager.svelte'
import { chatState } from './sharedChatState.svelte'
import { PLAN_MODE_MESSAGES } from './planModeMessages'
import { runChatLoop } from './chatLoop'
@@ -129,6 +130,9 @@ vi.mock('esm-env', async (importOriginal) => ({
}))
beforeEach(() => {
// These managers stand in for a mounted docked chat; without a layout to set
// it, sendRequest's "nowhere to render this turn" guard would refuse every send.
chatState.dockedChatAvailable = true
vi.clearAllMocks()
mocks.getCurrentModel.mockReturnValue(undefined)
mocks.tryGetCurrentModel.mockReturnValue(undefined)
@@ -172,6 +176,66 @@ function createFlowHelpers({
} as unknown as FlowAIChatHelpers
}
describe('AIChatManager unmounted-chat guard', () => {
// AI Sessions leave the docked pane unmounted, so an entry point that still
// drives this manager would otherwise stream and apply tool calls off-screen.
it('drops the turn when no chat UI is mounted, unless it is a session chat', async () => {
chatState.dockedChatAvailable = false
const docked = new AIChatManager()
docked.instructions = 'do a thing'
await docked.sendRequest()
expect(mocks.runChatLoop).not.toHaveBeenCalled()
const session = new AIChatManager()
session.isSessionChat = true
session.instructions = 'do a thing'
await session.sendRequest()
expect(mocks.runChatLoop).toHaveBeenCalled()
})
})
describe('AIChatManager.sendOrQueue', () => {
// The programmatic senders (an editor's "AI Fix", an arriving hand-off) have no
// composer to enforce the composer's rule for them: a second loop on one manager
// shares its abort controller and transcript.
it('queues instead of starting a second turn while one is streaming', () => {
const manager = new AIChatManager()
manager.loading = true
manager.sendOrQueue('fix the failing run')
expect(mocks.runChatLoop).not.toHaveBeenCalled()
expect(manager.queuedMessage).toBe('fix the failing run')
})
it('sends straight away when idle', async () => {
const manager = new AIChatManager()
manager.sendOrQueue('fix the failing run')
await vi.waitFor(() => expect(mocks.runChatLoop).toHaveBeenCalled())
expect(manager.queuedMessage).toBe('')
})
// `loading` only rises after a send's attachment upkeep, so gating on it alone
// leaves a window where a second programmatic send slips through.
it('queues during a send that has not reached loading yet', async () => {
const manager = new AIChatManager()
let releaseUpkeep: (() => void) | undefined
vi.spyOn(manager.attachedFiles, 'refreshFolders').mockImplementation(
() => new Promise<void>((resolve) => (releaseUpkeep = resolve))
)
manager.instructions = 'first turn'
const sending = manager.sendRequest()
await vi.waitFor(() => expect(manager.sendInFlight).toBe(true))
expect(manager.loading).toBe(false)
manager.sendOrQueue('fix the failing run')
expect(manager.queuedMessage).toBe('fix the failing run')
// Drain before leaving: a send still in flight would run its epilogue
// (queue flush included) inside whichever test happens to be next.
releaseUpkeep?.()
await sending
})
})
describe('AIChatManager request errors', () => {
const openaiModel = { provider: 'openai', model: 'gpt-4o' }
@@ -25,6 +25,16 @@ export function isGlobalAiEnabled(): boolean {
}
}
/**
* Whether an AI entry point hands off to a session instead of driving the docked
* chat. Deliberately the same condition as the root layout's `disableAi`, so a
* caller falling back on `false` always has a mounted pane to fall back to.
* Operators keep that pane (`/sessions` refuses them) until the operator chat ships.
*/
export function prefersSessionHandoff(isOperator: boolean | undefined): boolean {
return isGlobalAiEnabled() && !isOperator
}
/** Persist the opt-out choice, then hard-reload so every gated site re-reads it. */
export function setSessionsBetaOptOut(optOut: boolean, target: string) {
// Navigate even when persistence throws (quota, private browsing) — the
@@ -7,6 +7,11 @@ vi.mock('monaco-editor', () => ({
editor: {}
}))
vi.mock('$lib/utils/featureUsage', () => ({
logFeatureUsage: vi.fn(),
logHubScriptPick: vi.fn()
}))
const userHolder = vi.hoisted(() => ({
current: { is_super_admin: true } as { is_super_admin: boolean }
}))
@@ -902,6 +907,63 @@ describe('processToolCall', () => {
})
)
})
// The counter is silently dropped by the backend when the key is malformed, so nothing
// here fails loudly if a path stops logging or logs the wrong status.
it('logs one feature-usage outcome per tool call, keyed <tool>:<status>', async () => {
const { createToolDef, processToolCall } = await import('./shared')
const { logFeatureUsage } = await import('$lib/utils/featureUsage')
const outcomeKeys = async (
tool: Partial<import('./shared').Tool<any>> = {},
toolCallbacks: Partial<import('./shared').ToolCallbacks> = {}
) => {
vi.mocked(logFeatureUsage).mockClear()
await runToolCall(
{
def: createToolDef(z.object({}), 'run_script', 'Run script'),
fn: vi.fn().mockResolvedValue('done'),
...tool
},
toolCallbacks
)
return vi
.mocked(logFeatureUsage)
.mock.calls.map(([feature, kind, opts]) => [feature, kind, opts?.key])
}
expect(await outcomeKeys()).toEqual([['ai_chat', 'tool', 'run_script:ok']])
expect(await outcomeKeys({ fn: vi.fn().mockRejectedValue(new Error('boom')) })).toEqual([
['ai_chat', 'tool', 'run_script:error']
])
expect(await outcomeKeys({ validateBeforeConfirmation: () => 'not deployed' })).toEqual([
['ai_chat', 'tool', 'run_script:rejected']
])
expect(
await outcomeKeys(
{ requiresConfirmation: true },
{ requestConfirmation: vi.fn().mockResolvedValue(false) }
)
).toEqual([['ai_chat', 'tool', 'run_script:declined']])
expect(await outcomeKeys({}, { isPlanModeActive: () => true })).toEqual([
['ai_chat', 'tool', 'run_script:blocked_plan_mode']
])
// A name the model invented resolves to no tool, and must never reach telemetry.
vi.mocked(logFeatureUsage).mockClear()
await processToolCall({
tools: [{ def: createToolDef(z.object({}), 'run_script', 'Run script'), fn: vi.fn() }],
toolCall: {
id: 'call_ghost',
type: 'function',
function: { name: 'hallucinated_tool', arguments: '{}' }
},
helpers: {},
workspace: 'test-workspace',
toolCallbacks: { setToolStatus: vi.fn(), removeToolStatus: vi.fn() }
})
expect(logFeatureUsage).not.toHaveBeenCalled()
})
})
async function runToolCall(
@@ -797,6 +797,15 @@ function stringifyErrorBody(body: unknown): string {
}
}
/**
* Closed vocabulary for the `ai_chat`/`tool` counter's `<name>:<status>` key.
* `ok` means the tool function resolved tools that report failure by returning an
* error string instead of throwing land there too. A call abandoned mid-execution
* (tab closed while a tool polls) logs nothing, so the statuses sum to the calls that
* finished, not to the calls made.
*/
type ToolCallStatus = 'ok' | 'error' | 'declined' | 'rejected' | 'blocked_plan_mode'
export async function processToolCall<T>({
tools,
toolCall,
@@ -810,10 +819,24 @@ export async function processToolCall<T>({
toolCallbacks: ToolCallbacks
workspace?: string
}): Promise<ChatCompletionMessageParam> {
const tool = tools.find((t) => t.def.function.name === toolCall.function.name)
const workspaceId = workspace ?? get(workspaceStore) ?? ''
// Exactly once per call, on whichever path ends it. Keyed by the resolved tool's
// declared name, not the model-provided string, so hallucinated tool names never
// enter telemetry — an unresolved name is counted nowhere.
let outcomeLogged = false
const logToolOutcome = (status: ToolCallStatus) => {
if (!tool || outcomeLogged) return
outcomeLogged = true
logFeatureUsage('ai_chat', 'tool', {
key: `${tool.def.function.name}:${status}`,
workspace: workspaceId
})
}
try {
const args = JSON.parse(toolCall.function.arguments || '{}')
const tool = tools.find((t) => t.def.function.name === toolCall.function.name)
const workspaceId = workspace ?? get(workspaceStore) ?? ''
// Fails closed: untagged is blocked, only the safety tag exempt. Runs before anything
// belonging to the tool, so a validator cannot probe while planning — and again after
@@ -830,6 +853,7 @@ export async function processToolCall<T>({
: { label: PLAN_MODE_MESSAGES.blockedLabel, result: PLAN_MODE_MESSAGES.blockedResult }
if (!refusal) return undefined
toolCallbacks.onToolBlockedByPlanMode?.()
logToolOutcome('blocked_plan_mode')
toolCallbacks.setToolStatus(toolCall.id, {
content: refusal.label,
parameters: args,
@@ -858,6 +882,7 @@ export async function processToolCall<T>({
await tool?.validateBeforeConfirmation?.({ args, workspace: workspaceId, helpers })
)
if (rejection) {
logToolOutcome('rejected')
toolCallbacks.setToolStatus(toolCall.id, {
content: rejection.label,
parameters: args,
@@ -912,6 +937,7 @@ export async function processToolCall<T>({
const confirmed = await toolCallbacks.requestConfirmation(toolCall.id, toolCall.function.name)
if (!confirmed) {
logToolOutcome('declined')
toolCallbacks.setToolStatus(toolCall.id, {
content: 'Cancelled by user',
isLoading: false,
@@ -940,11 +966,6 @@ export async function processToolCall<T>({
}
let result = ''
// Key by the resolved tool's declared name, not the model-provided string,
// so hallucinated tool names never enter telemetry.
if (tool) {
logFeatureUsage('ai_chat', 'tool', { key: tool.def.function.name, workspace: workspaceId })
}
try {
result = await callTool({
tools,
@@ -955,12 +976,14 @@ export async function processToolCall<T>({
toolCallbacks,
toolId: toolCall.id
})
logToolOutcome('ok')
toolCallbacks.setToolStatus(toolCall.id, {
isLoading: false,
isStreamingArguments: false
})
} catch (err) {
console.error(err)
logToolOutcome('error')
const errorMessage = formatToolError(err)
toolCallbacks.setToolStatus(toolCall.id, {
isLoading: false,
@@ -977,6 +1000,7 @@ export async function processToolCall<T>({
return toAdd
} catch (err) {
console.error(err)
logToolOutcome('error')
const errorMessage = formatToolError(err)
toolCallbacks.setToolStatus(toolCall.id, {
isLoading: false,
@@ -35,6 +35,9 @@
import { Button } from '../common'
import { MousePointerClick, X } from 'lucide-svelte'
import FlowPanelPlacementPicker from './common/FlowPanelPlacementPicker.svelte'
import { prefersSessionHandoff } from '../copilot/chat/global/gate'
import { openSourceInSession } from '$lib/components/sessions/sessionSwitch.svelte'
import { userStore } from '$lib/stores'
const { flowStore, selectionManager } = getContext<FlowEditorContext>('FlowEditorContext')
const sessionScopedManager = getContext<AIChatManager>('aiChatManager')
const aiChatManager = sessionScopedManager ?? singletonAiChatManager
@@ -273,6 +276,12 @@
aiChatManager.flowOptions = options
})
// The step exists but is empty, so name it: a GLOBAL-mode request carries no
// implicit "current step" the way the old SCRIPT-mode generateStep did.
function stepInstructionsPrompt(moduleId: string, instructions: string): string {
return `Write the code for step \`${moduleId}\` of the flow open in the editor:\n\n${instructions}`
}
onMount(() => {
if (modalPanel) {
selectionManager.setOnSelectIntent((id, opts) => {
@@ -368,6 +377,32 @@
{showJobStatus}
on:reload
on:generateStep={({ detail }) => {
// The step is already inserted; the prompt describes what it should
// contain. Hand it to a session opened on that step rather than the
// docked chat, which sessions leave unmounted. Sent on arrival: the
// user already said what they wanted in the description field.
if (
!sessionScopedManager &&
sessionOpen &&
prefersSessionHandoff($userStore?.operator)
) {
void openSourceInSession(sessionOpen, {
previewParams: { selected: detail.moduleId },
seedPrompt: stepInstructionsPrompt(detail.moduleId, detail.instructions),
autoSend: true
})
return
}
// Already in a session: its chat is on screen, so ask it directly.
// Not `generateStep` — that forces the request into SCRIPT mode, and
// changeMode is persistent, so it would strand the session outside
// GLOBAL. Global mode writes step code through set_flow_module_code.
if (sessionScopedManager) {
sessionScopedManager.sendOrQueue(
stepInstructionsPrompt(detail.moduleId, detail.instructions)
)
return
}
if (!aiChatManager.open) {
aiChatManager.openChat()
}
@@ -5,16 +5,18 @@
import { getContext } from 'svelte'
import { type TriggerContext } from '$lib/components/triggers'
import { enterpriseLicense } from '$lib/stores'
import {
MqttIcon,
AmqpIcon,
NatsIcon,
KafkaIcon,
AwsIcon,
GoogleCloudIcon
} from '$lib/components/icons'
import MqttIcon from '$lib/components/icons/MqttIcon.svelte'
import AmqpIcon from '$lib/components/icons/AmqpIcon.svelte'
import NatsIcon from '$lib/components/icons/NatsIcon.svelte'
import KafkaIcon from '$lib/components/icons/KafkaIcon.svelte'
import AwsIcon from '$lib/components/icons/AwsIcon.svelte'
import GoogleCloudIcon from '$lib/components/icons/GoogleCloudIcon.svelte'
import AzureIcon from '$lib/components/icons/AzureIcon.svelte'
import { type Trigger, type TriggerType } from '$lib/components/triggers/utils'
import {
triggerIconMapMono,
type Trigger,
type TriggerType
} from '$lib/components/triggers/utils'
import { Menu, Menubar, MeltButton, MenuItem, Tooltip } from '$lib/components/meltComponents'
import { twMerge } from 'tailwind-merge'
import SchedulePollIcon from '$lib/components/icons/SchedulePollIcon.svelte'
@@ -320,10 +322,13 @@
{/snippet}
{#snippet simpleTriggerItem({ item, type })}
{@const { icon: SvelteComponent, countKey } = triggerTypeConfig()[type] || {
{@const { icon: ColourIcon, countKey } = triggerTypeConfig()[type] || {
icon: Database,
countKey: undefined
}}
<!-- The badge shows the full-colour mark; the menu it opens is a dense list beside lucide
glyphs, so its rows use the desaturated variants. See icons/index.ts. -->
{@const SvelteComponent = triggerIconMapMono[type] ?? ColourIcon}
<MenuItem {item} class={itemClass}>
<div class="flex flex-row items-center gap-2">
<SvelteComponent size={14} />
@@ -0,0 +1,59 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<!-- #FF5416 → #FF0000 gradient on both themes per brand.ably.com/logo. "Don't use other colours or gradients for the symbol." -->
<svg
{width}
{height}
viewBox="0 0 77.5931 64"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
d="M38.5717 0L6.29575 59.0738L0 54.6586L29.8639 0H38.5717ZM39.0214 0L71.2973 59.0738L77.5931 54.6586L47.7292 0H39.0214Z"
fill="url(#ably-motif-upper)"
/>
<path
d="M70.8476 59.4213L38.7965 34.3201L6.74542 59.4213L13.2865 64L38.7965 44.0294L64.3066 64L70.8476 59.4213Z"
fill="url(#ably-motif-lower)"
/>
<defs>
<linearGradient
id="ably-motif-upper"
x1="10.9472"
y1="74.8439"
x2="64.9206"
y2="14.9005"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#FF5416" />
<stop offset="0.2535" stop-color="#FF5115" />
<stop offset="0.461" stop-color="#FF4712" />
<stop offset="0.6523" stop-color="#FF350E" />
<stop offset="0.8327" stop-color="#FF1E08" />
<stop offset="1" stop-color="#FF0000" />
</linearGradient>
<linearGradient
id="ably-motif-lower"
x1="21.4168"
y1="78.7187"
x2="53.3166"
y2="43.2904"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#FF5416" />
<stop offset="0.2535" stop-color="#FF5115" />
<stop offset="0.461" stop-color="#FF4712" />
<stop offset="0.6523" stop-color="#FF350E" />
<stop offset="0.8327" stop-color="#FF1E08" />
<stop offset="1" stop-color="#FF0000" />
</linearGradient>
</defs>
</svg>
@@ -0,0 +1,30 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<!-- #20E492 per abstractapi.com's own logo SVG (6538df34291c9fa4ed28d6f7_Logo.svg). -->
<svg {width} {height} viewBox="0.77 0 90.15 90.15" xmlns="http://www.w3.org/2000/svg">
<defs
><clipPath id="abstractapi-clip0_4255_674">
<rect
width="90.1487"
height="90.1487"
fill="white"
transform="translate(0.769531 0.000732422)"
/>
</clipPath></defs
>
<g clip-path="url(#abstractapi-clip0_4255_674)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M84.2021 0.000732422H7.43104C3.77267 0.000732422 0.769531 3.00387 0.769531 6.66224V83.4333C0.769531 87.1463 3.77267 90.1495 7.43104 90.1495H20.6994C23.8664 89.9856 25.9413 88.7844 26.9787 86.4365C29.545 78.4099 37.0802 72.6221 45.9804 72.6221C54.8806 72.6221 62.4158 78.4099 65.0367 86.4365C66.0741 88.7844 68.149 89.9856 71.316 90.1495H84.2021C87.9151 90.1495 90.9183 87.1463 90.9183 83.4333V6.66224C90.9183 3.00387 87.9151 0.000732422 84.2021 0.000732422ZM64.1084 53.8388H27.5248C26.5419 53.8388 25.8867 52.7467 26.3781 51.8185L44.6699 20.1491C45.216 19.2754 46.4718 19.2754 47.0178 20.1491L65.3097 51.8185C65.8011 52.7467 65.1459 53.8388 64.1084 53.8388Z"
fill="#20E492"
/>
</g>
</svg>
@@ -0,0 +1,23 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<!-- #4C49CB, with #F6511E and #CAD1E1, per Accelo_Logo-Primary.svg on accelo.com. The mark keeps these
three fills in both themes; only the wordmark (not drawn here) swaps #10202D for white. -->
<svg {width} {height} viewBox="-1 -1 158 152" xmlns="http://www.w3.org/2000/svg">
<path
d="M95.1552 97.2397C92.0269 90.7038 84.2061 87.9534 77.6839 91.0587L7.56272 124.655C0.922451 127.849 -1.8517 135.804 1.30611 142.429C3.60807 147.22 8.35955 150 13.3176 150C15.2359 150 17.2132 149.586 19.0725 148.699L89.1642 115.162C95.6864 112.027 98.4606 104.19 95.3323 97.6537C95.2733 97.5354 95.2142 97.4171 95.1552 97.2988V97.2397Z"
fill="#CAD1E1"
/><path
d="M13.2881 150C11.1632 150 9.03834 149.497 7.0315 148.433C0.538796 144.972 -1.88121 136.899 1.57173 130.392L67.2956 7.06827C70.7485 0.561959 78.8054 -1.86312 85.2981 1.59705C91.7908 5.05723 94.2108 13.131 90.7579 19.6373L25.034 142.932C22.6435 147.427 18.0396 150 13.2881 150Z"
fill="#4C49CB"
/><path
d="M143.378 150C138.597 150 133.964 147.397 131.573 142.843L67.2366 19.5189C63.8427 13.0126 66.3512 4.93885 72.844 1.53782C79.3367 -1.8632 87.3935 0.650598 90.7874 7.15691L155.154 130.481C158.548 136.987 156.039 145.061 149.546 148.462C147.569 149.497 145.474 149.97 143.378 149.97V150Z"
fill="#F6511E"
/>
</svg>
@@ -0,0 +1,23 @@
<script lang="ts">
import BrandLetterIcon from './BrandLetterIcon.svelte'
interface Props {
height?: string
width?: string
size?: number
}
let { height = '24px', width = '24px', size = undefined }: Props = $props()
</script>
<!-- Actimo publishes no vector mark and no brand palette; its favicon.ico is zero bytes and the
pinwheel exists only as raster (actimo.com, now a Kahoot! company). Letter inherits the
surrounding text colour rather than inventing a brand colour. -->
<BrandLetterIcon
letter="A"
bgClass="fill-transparent"
textClass="text-secondary"
{height}
{width}
{size}
/>
@@ -7,9 +7,17 @@
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg {width} {height} viewBox="0 0 256 391" xmlns="http://www.w3.org/2000/svg">
<!-- #004CFF / #FFFFFF per activecampaign.com/brand logo pack (ActiveCampaign-Glyph-Blue.svg /
ActiveCampaign-Glyph-White.svg). -->
<svg
class="text-[#004CFF] dark:text-[#FFFFFF]"
{width}
{height}
viewBox="0 0 256 391"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="#004CFF"
d="m1.273 0l3.049 2.142c9.488 6.603 232.185 161.826 238.397 166.475c8.795 6.174 13.281 13.81 13.281 22.668v4.397c-.1 6.842-2.558 16.469-13.28 24.52l-.039.026c-5.166 3.692-57.747 40.12-113.415 78.701c-4.9 3.397-9.858 6.832-14.827 10.275l-3.314 2.297C61.96 345.572 12.62 379.777 5.947 384.498v6.136l-3.641-6.124c-.177-.315-.34-.58-.151-1.008H2.04v-32.03c0-10.924 1.21-16.178 13.37-24.28c5.179-3.33 49.529-33.955 94.578-65.128l5.3-3.668c41.513-28.73 82.338-57.033 92.364-63.974C186.484 179.718 27.796 69.554 16.923 61.59l-1.286-.932C7.598 54.836 1.273 50.25 1.273 38.128zM21.86 111.361c10.736 6.905 114.562 78.954 115.608 79.697l2.356 1.626l-2.394 1.6s-7.018 4.675-14.805 10.118c-6.666 4.889-12.903 7.333-19.102 7.333c-5.506 0-10.962-1.915-16.708-5.733c-6.12-4.086-23.363-15.98-40.816-28.064l-2.38-1.648A27213 27213 0 0 1 .832 146.592l-.832-.58v-24.47c0-5.62 2.42-10.067 6.666-12.222c4.548-2.318 10.231-1.562 15.195 2.041"
/>
</svg>
@@ -1,12 +1,13 @@
<script lang="ts">
interface Props {
height?: string;
width?: string;
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props();
let { height = '24px', width = '24px' }: Props = $props()
</script>
<!-- #F1007E (grey half #6D6D6D) per activitypub.rocks/static/images/ActivityPub-logo.svg. -->
<svg
x="0px"
y="0px"
@@ -0,0 +1,407 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<!-- #149ED7 per Acumbamail's own isotype SVG, /static/favico/Acumbamail/favicon-32.svg on acumbamail.com. -->
<svg {width} {height} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="acumbamail-clippath">
<path
fill="none"
d="M19.88,24.42c-.07.36-.09.39-.09.39l8.64,2.75h.1c.06-.07.25-.29.4-.5.17-.23.25-.37.25-.37l-9.21-2.81s-.03.25-.09.54h0Z"
/>
</clipPath>
<linearGradient
id="acumbamail-Degradado_sin_nombre_14"
data-name="Degradado sin nombre 14"
x1="3092.71"
y1="-1703.57"
x2="3093.22"
y2="-1703.57"
gradientTransform="translate(-42784.62 23603.13) scale(13.84)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stop-color="#e62f71" />
<stop offset=".11" stop-color="#e62f71" />
<stop offset=".71" stop-color="#ea5d52" />
<stop offset=".86" stop-color="#ec6a4a" />
<stop offset="1" stop-color="#ec6a4a" />
</linearGradient>
<clipPath id="acumbamail-clippath-1">
<polygon fill="none" points=".04 9.32 10.75 10.49 10.75 9.43 .04 7.99 .04 9.32" />
</clipPath>
<linearGradient
id="acumbamail-Degradado_sin_nombre_7"
data-name="Degradado sin nombre 7"
x1="3183.18"
y1="-1671.62"
x2="3183.69"
y2="-1671.62"
gradientTransform="translate(-67072.09 35230.2) scale(21.07)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stop-color="#149ed7" />
<stop offset=".44" stop-color="#45afdf" />
<stop offset=".8" stop-color="#68bbe5" />
<stop offset="1" stop-color="#76c0e8" />
</linearGradient>
<clipPath id="acumbamail-clippath-2">
<polygon fill="none" points=".04 16.95 .04 17.86 10.75 16.95 10.75 16.13 .04 16.95" />
</clipPath>
<linearGradient
id="acumbamail-Degradado_sin_nombre_8"
data-name="Degradado sin nombre 8"
x1="3183.18"
y1="-1672"
x2="3183.69"
y2="-1672"
gradientTransform="translate(-67072.09 35246.13) scale(21.07)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stop-color="#149ed7" />
<stop offset=".54" stop-color="#078bc8" />
<stop offset="1" stop-color="#0080c0" />
</linearGradient>
<clipPath id="acumbamail-clippath-3">
<path
fill="none"
d="M1.25,26.02s.14.26.45.7c.14.21.24.33.28.38h.08l9.17-3.75s-.09-.24-.17-.57c-.09-.36-.16-.78-.16-.78L1.25,26.02h0Z"
/>
</clipPath>
<linearGradient
id="acumbamail-Degradado_sin_nombre_10"
data-name="Degradado sin nombre 10"
x1="3245.89"
y1="-1870.85"
x2="3246.4"
y2="-1870.85"
gradientTransform="translate(-44239.32 25524.21) scale(13.63)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stop-color="#0463a9" />
<stop offset="1" stop-color="#0080c0" />
</linearGradient>
<clipPath id="acumbamail-clippath-4">
<path
fill="none"
d="M19.53,18.03s.09.43.14.71c.05.28,9.82-1.38,9.82-1.38,0,0-.11-.55-.17-.8,0-.02-.05-.02-.14-.02-1.18,0-9.66,1.49-9.66,1.49h0Z"
/>
</clipPath>
<linearGradient
id="acumbamail-Degradado_sin_nombre_5"
data-name="Degradado sin nombre 5"
x1="3082.78"
y1="-1704.52"
x2="3083.29"
y2="-1704.52"
gradientTransform="translate(-41380.04 22909.51) scale(13.43)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stop-color="#f9b434" />
<stop offset=".05" stop-color="#f9b434" />
<stop offset=".3" stop-color="#f4983c" />
<stop offset=".74" stop-color="#ec6a4a" />
<stop offset="1" stop-color="#ec6a4a" />
</linearGradient>
<clipPath id="acumbamail-clippath-5">
<path
fill="none"
d="M18.72,8.05c-.03.07-.16.31-.29.68-.15.42-.23.8-.23.8l10.4,2.74s0-.27.09-.59c.03-.11.07-.22.07-.22l-9.97-3.41h-.06Z"
/>
</clipPath>
<linearGradient
id="acumbamail-Degradado_sin_nombre_11"
data-name="Degradado sin nombre 11"
x1="3099.34"
y1="-1699.79"
x2="3099.85"
y2="-1699.79"
gradientTransform="translate(-44054.21 24181.14) scale(14.22)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stop-color="#f9b434" />
<stop offset=".16" stop-color="#f9b434" />
<stop offset=".85" stop-color="#ffd634" />
<stop offset="1" stop-color="#ffd634" />
</linearGradient>
<clipPath id="acumbamail-clippath-6">
<path
fill="none"
d="M8.7.6C3.33.6.4,2.54.04,6.71v1.88l11.06,1.35h2.6v-5.32h-2.95V.6h-2.06Z"
/>
</clipPath>
<linearGradient
id="acumbamail-Degradado_sin_nombre_7-2"
data-name="Degradado sin nombre 7"
x1="3219.1"
y1="-1657.94"
x2="3219.61"
y2="-1657.94"
gradientTransform="translate(-86464.4 44537.65) scale(26.86)"
xlink:href="#acumbamail-Degradado_sin_nombre_7"
/>
<clipPath id="acumbamail-clippath-7">
<polygon
fill="none"
points=".04 8.59 .04 17.33 10.75 16.58 10.75 9.98 10.75 9.93 11.1 9.93 .11 8.59 .04 8.59"
/>
</clipPath>
<linearGradient
id="acumbamail-Degradado_sin_nombre_8-2"
data-name="Degradado sin nombre 8"
x1="3167.2"
y1="-1676.56"
x2="3167.71"
y2="-1676.56"
gradientTransform="translate(-61160.54 32387.24) scale(19.31)"
xlink:href="#acumbamail-Degradado_sin_nombre_8"
/>
<clipPath id="acumbamail-clippath-8">
<path
fill="none"
d="M1.69,26.73c1.63,2.31,4.31,3.79,7.49,4.36h.32c.9-.23,6.04-1.68,7.78-5.06-.39.06-1.2.14-1.69.14-2.82,0-4.05-1.41-4.54-3.37l-9.36,3.94h0Z"
/>
</clipPath>
<linearGradient
id="acumbamail-Degradado_sin_nombre_9"
data-name="Degradado sin nombre 9"
x1="3103.14"
y1="-1529.8"
x2="3103.65"
y2="-1529.8"
gradientTransform="translate(-22294.99 -45217.7) rotate(90) scale(14.58)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stop-color="#05488a" />
<stop offset=".83" stop-color="#2076b8" />
<stop offset="1" stop-color="#2680c2" />
</linearGradient>
<clipPath id="acumbamail-clippath-9">
<path
fill="none"
d="M.04,17.33v4.69c.15,1.83.73,3.4,1.65,4.7l9.36-3.94c-.22-.88-.3-1.87-.3-2.89v-3.32L.04,17.33h0Z"
/>
</clipPath>
<linearGradient
id="acumbamail-Degradado_sin_nombre_10-2"
data-name="Degradado sin nombre 10"
x1="3276.47"
y1="-1797.36"
x2="3276.98"
y2="-1797.36"
gradientTransform="translate(-62253.18 34171.52) scale(19)"
xlink:href="#acumbamail-Degradado_sin_nombre_10"
/>
<clipPath id="acumbamail-clippath-10">
<path
fill="none"
d="M24.36,4.64c-2.98.22-5.12,1.81-5.93,4.1l10.25,2.95c.33-1.35,1.54-1.78,3.28-1.76v-5.29h-7.6Z"
/>
</clipPath>
<linearGradient
id="acumbamail-Degradado_sin_nombre_11-2"
data-name="Degradado sin nombre 11"
x1="3154.24"
y1="-1679.36"
x2="3154.75"
y2="-1679.36"
gradientTransform="translate(-57573.98 30673.19) scale(18.26)"
xlink:href="#acumbamail-Degradado_sin_nombre_11"
/>
<clipPath id="acumbamail-clippath-11">
<path
fill="none"
d="M18.42,8.76c-.4,1.13-.48,2.41-.17,3.79.49,2.24.99,4.01,1.34,5.78h.03l9.78-1.37c-.35-1.72-.69-3.19-.75-3.58-.1-.67-.09-1.23.03-1.68l-10.2-2.93h-.06Z"
/>
</clipPath>
<linearGradient
id="acumbamail-Degradado_sin_nombre_12"
data-name="Degradado sin nombre 12"
x1="3116.51"
y1="-1693.09"
x2="3117.02"
y2="-1693.09"
gradientTransform="translate(-47632.82 25900.89) scale(15.29)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stop-color="#f8b233" />
<stop offset=".05" stop-color="#f8b233" />
<stop offset=".21" stop-color="#f5a038" />
<stop offset=".74" stop-color="#ec694b" />
<stop offset="1" stop-color="#ec694b" />
</linearGradient>
<clipPath id="acumbamail-clippath-12">
<path
fill="none"
d="M13.02,31.08l.08.31h6.02c3.92-.36,7.83-1.61,9.82-4.34l-9.05-2.64c-.59,2.89-2.81,5.19-6.86,6.66Z"
/>
</clipPath>
<linearGradient
id="acumbamail-Degradado_sin_nombre_13"
data-name="Degradado sin nombre 13"
x1="3367.14"
y1="-1853.65"
x2="3367.65"
y2="-1853.65"
gradientTransform="translate(-53254.17 29352.69) scale(15.82)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stop-color="#be0c6e" />
<stop offset=".51" stop-color="#d41f71" />
<stop offset=".89" stop-color="#e62f71" />
<stop offset="1" stop-color="#e62f71" />
</linearGradient>
<clipPath id="acumbamail-clippath-13">
<path
fill="none"
d="M19.59,18.32c.27,1.39.45,2.81.45,4.46,0,.56-.05,1.11-.16,1.63l8.97,2.61h.1c.83-1.16,1.32-2.58,1.32-4.32,0-1.28-.45-3.7-.88-5.76l-9.81,1.37h0Z"
/>
</clipPath>
<linearGradient
id="acumbamail-Degradado_sin_nombre_14-2"
data-name="Degradado sin nombre 14"
x1="3124.76"
y1="-1750.32"
x2="3125.27"
y2="-1750.32"
gradientTransform="translate(-44849.16 25156.63) scale(14.36)"
xlink:href="#acumbamail-Degradado_sin_nombre_14"
/>
</defs>
<g clip-path="url(#acumbamail-clippath)">
<rect
fill="url(#acumbamail-Degradado_sin_nombre_14)"
x="19.79"
y="23.88"
width="9.4"
height="3.69"
/>
</g>
<g clip-path="url(#acumbamail-clippath-1)">
<rect
fill="url(#acumbamail-Degradado_sin_nombre_7)"
x=".04"
y="7.99"
width="10.71"
height="2.49"
/>
</g>
<g clip-path="url(#acumbamail-clippath-2)">
<rect
fill="url(#acumbamail-Degradado_sin_nombre_8)"
x=".04"
y="16.13"
width="10.71"
height="1.73"
/>
</g>
<g clip-path="url(#acumbamail-clippath-3)">
<rect
fill="url(#acumbamail-Degradado_sin_nombre_10)"
x="1.01"
y="19.14"
width="10.45"
height="10.84"
transform="translate(-16.23 12.84) rotate(-48.2)"
/>
</g>
<g clip-path="url(#acumbamail-clippath-4)">
<rect
fill="url(#acumbamail-Degradado_sin_nombre_5)"
x="19.53"
y="16.54"
width="9.96"
height="2.48"
/>
</g>
<g clip-path="url(#acumbamail-clippath-5)">
<rect
fill="url(#acumbamail-Degradado_sin_nombre_11)"
x="18.2"
y="8.05"
width="10.55"
height="4.22"
/>
</g>
<g clip-path="url(#acumbamail-clippath-6)">
<rect
fill="url(#acumbamail-Degradado_sin_nombre_7-2)"
x=".04"
y=".6"
width="13.66"
height="9.33"
/>
</g>
<g clip-path="url(#acumbamail-clippath-7)">
<rect
fill="url(#acumbamail-Degradado_sin_nombre_8-2)"
x=".04"
y="8.59"
width="11.06"
height="8.74"
/>
</g>
<g clip-path="url(#acumbamail-clippath-8)">
<rect
fill="url(#acumbamail-Degradado_sin_nombre_9)"
x="1.47"
y="18.22"
width="16.03"
height="17.43"
transform="translate(-17.71 18.21) rotate(-52.8)"
/>
</g>
<g clip-path="url(#acumbamail-clippath-9)">
<rect
fill="url(#acumbamail-Degradado_sin_nombre_10-2)"
x="-1.9"
y="14.17"
width="14.9"
height="14.97"
transform="translate(-14.29 11.36) rotate(-48.2)"
/>
</g>
<g clip-path="url(#acumbamail-clippath-10)">
<rect
fill="url(#acumbamail-Degradado_sin_nombre_11-2)"
x="18.42"
y="4.64"
width="13.54"
height="7.05"
/>
</g>
<g clip-path="url(#acumbamail-clippath-11)">
<rect
fill="url(#acumbamail-Degradado_sin_nombre_12)"
x="17.94"
y="8.76"
width="11.46"
height="9.56"
/>
</g>
<g clip-path="url(#acumbamail-clippath-12)">
<rect
fill="url(#acumbamail-Degradado_sin_nombre_13)"
x="15.43"
y="19.3"
width="11.09"
height="17.22"
transform="translate(-11.63 40.38) rotate(-74)"
/>
</g>
<g clip-path="url(#acumbamail-clippath-13)">
<rect
fill="url(#acumbamail-Degradado_sin_nombre_14-2)"
x="18.63"
y="15.93"
width="12.59"
height="12.13"
transform="translate(-4.11 5.82) rotate(-12.3)"
/>
</g>
</svg>
@@ -0,0 +1,22 @@
<script lang="ts">
import BrandLetterIcon from './BrandLetterIcon.svelte'
interface Props {
height?: string
width?: string
size?: number
}
let { height = '24px', width = '24px', size = undefined }: Props = $props()
</script>
<!-- #7EA2EA sampled from adrapid.com's own app favicon; they publish no vector mark or palette.
Light text darkened from it for legibility. -->
<BrandLetterIcon
letter="A"
bgClass="fill-transparent dark:fill-[#7EA2EA]"
textClass="text-[#5B89E4] dark:text-[#111111]"
{height}
{width}
{size}
/>
@@ -0,0 +1,25 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<!-- #00acc6 per adhook's own logo (https://adhook.io/fr/images/logo.svg, `.cls-1{fill:#00acc6}`). -->
<svg
{width}
{height}
viewBox="143 17 71 42"
fill="#00acc6"
fill-rule="evenodd"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M191.19,40.77l-5.74-6.27-7.66,7.63h-.11l-4.35,4.33a12.4,12.4,0,0,1-17.2.17A11.56,11.56,0,0,1,156,30a12.44,12.44,0,0,1,11.76-3.18l6.59-6.58A21.13,21.13,0,0,0,149.83,24a20,20,0,0,0,.3,28.77,21.09,21.09,0,0,0,29.33-.29Z"
/>
<path
d="M165.59,35.66l5.74,6.27L179,34.3h.11L183.44,30a12.4,12.4,0,0,1,17.21-.18,11.57,11.57,0,0,1,.18,16.65,12.45,12.45,0,0,1-11.76,3.18l-6.59,6.58A21.13,21.13,0,0,0,207,52.46a20,20,0,0,0-.3-28.78,21.11,21.11,0,0,0-29.34.3Z"
/>
</svg>
@@ -1,24 +1,24 @@
<script lang="ts">
import BrandLetterIcon from './BrandLetterIcon.svelte'
interface Props {
height?: string;
width?: string;
height?: string
width?: string
size?: number
}
let { height = '24px', width = '24px' }: Props = $props();
let { height = '24px', width = '24px', size = undefined }: Props = $props()
</script>
<svg {width} {height} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect x="1.5" y="1.5" width="21" height="21" rx="4.5" fill="#EB1000" />
<path
fill="#fff"
fill-rule="evenodd"
d="M10.7 5.8h2.6L17.6 16h-2.7l-.86-2.3h-4.08L9.1 16H6.4L10.7 5.8Zm1.3 2.9-1.4 3.7h2.8L12 8.7Z"
/>
<path
d="M6.6 18.7c1.5.85 2.9.2 4.1-.45s2.5-1.05 4-.2"
stroke="#fff"
stroke-width="1.15"
stroke-linecap="round"
fill="none"
/>
</svg>
<!-- #584CCC per adobe.com/cc-shared/assets/img/product-icons/svg/acrobat-sign.svg. Adobe's mark
itself is not shipped: "Adobe does not allow the use of its product icons by third parties in
their products or related materials of any kind, except through an Adobe partnership
agreement" (adobe.com/legal/permissions/icons-web-logos.html). -->
<BrandLetterIcon
letter="A"
bgClass="fill-transparent dark:fill-[#584CCC]"
textClass="text-[#584CCC] dark:text-white"
{height}
{width}
{size}
/>
@@ -0,0 +1,21 @@
<script lang="ts">
import BrandLetterIcon from './BrandLetterIcon.svelte'
interface Props {
height?: string
width?: string
size?: number
}
let { height = '24px', width = '24px', size = undefined }: Props = $props()
</script>
<!-- #2EA8D7 per aeroworkflow.com's schema.org Organization logo; the mark itself is raster only. -->
<BrandLetterIcon
letter="A"
bgClass="fill-transparent dark:fill-[#2EA8D7]"
textClass="text-[#2596C1] dark:text-[#111111]"
{height}
{width}
{size}
/>
@@ -0,0 +1,27 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
{width}
{height}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M15 12h-5" />
<path d="M15 8h-5" />
<path d="M19 17V5a2 2 0 0 0-2-2H4" />
<path
d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"
/>
</svg>
@@ -0,0 +1,24 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<!-- #000000 / #FFFFFF per ai21.com (ai21-logo-black.svg / ai21-logo-white.svg). -->
<svg
class="text-[#000000] dark:text-[#FFFFFF]"
{width}
{height}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<g transform="translate(-0.906 7.685) scale(0.3596)">
<path
d="M38.6929 22.5785H55.9802V17.138H51.9975V18.6189H45.7824L51.3942 15.3551C53.2042 14.2975 55.4669 12.2117 55.4669 8.79645C55.4669 4.44395 51.7863 1.21045 47.2003 1.21045C42.313 1.21045 38.7224 4.92774 38.7527 9.6126H43.1873V9.43088C43.1873 7.04302 44.9073 5.35095 47.2298 5.35095C49.5523 5.35095 51.1519 6.92267 51.1519 8.94789C51.1519 9.94495 50.7295 10.9731 49.2207 11.9096L38.6913 18.5886V22.5777L38.6929 22.5785ZM58.1521 22.5785H66.4785V18.5894H64.5776V1.42166H58.1513V5.41073H60.0825V18.5878H58.1513V22.5769L58.1521 22.5785ZM28.4353 22.5785H36.4908V18.5894H34.6505V5.41152H36.4908V1.42166H28.4353V5.41073H30.2756V18.5878H28.4353V22.5769V22.5785ZM13.3199 18.4977C9.75963 18.4977 6.83378 15.5663 6.83378 11.9694C6.83378 8.37244 9.76043 5.4713 13.3199 5.4713C16.8794 5.4713 19.7462 8.37244 19.7462 11.9694C19.7462 15.5663 16.8802 18.4977 13.3199 18.4977ZM13.3199 22.7897C15.6129 22.7897 17.6644 22.0644 19.233 20.6744L19.4139 20.523V22.5785H25.9303V18.5894H23.9991V5.41152H25.9303V1.42166H19.4139V3.41659L19.233 3.26516C17.6342 1.93573 15.6129 1.21045 13.3199 1.21045C7.40684 1.21045 2.51953 6.04595 2.51953 11.9997C2.51953 17.9534 7.40684 22.7889 13.3199 22.7889V22.7897Z"
></path>
</g>
</svg>
@@ -0,0 +1,27 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
{width}
{height}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 8V4H8" />
<rect width="16" height="12" x="4" y="8" rx="2" />
<path d="M2 14h2" />
<path d="M20 14h2" />
<path d="M15 13v2" />
<path d="M9 13v2" />
</svg>
@@ -1,22 +1,31 @@
<script lang="ts">
interface Props {
height?: string;
width?: string;
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props();
let { height = '24px', width = '24px' }: Props = $props()
</script>
<!-- #FCB400 per airtable.com/favicon.ico (fixed full-colour mark: #18BFFF and #F82B60 panels). -->
<svg
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
{width}
{height}
viewBox="0 0 50 50"
style="fill:currentcolor;"
viewBox="0 41.8 512 428.4"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M 24.505859 4.0078125 C 23.980484 4.0080625 23.454344 4.1114062 22.964844 4.3164062 L 4.2226562 11.646484 C 3.4796562 11.959484 3 12.683281 3 13.488281 C 2.999 14.293281 3.4771406 15.017797 4.2441406 15.341797 L 23.236328 22.775391 C 23.613328 22.935391 24.018969 23.013672 24.417969 23.013672 C 24.802969 23.013672 25.181391 22.939875 25.525391 22.796875 L 45.771484 15.330078 C 46.519484 15.018078 47.001953 14.292422 47.001953 13.482422 C 47.000953 12.672422 46.516422 11.949047 45.732422 11.623047 L 26.046875 4.3144531 C 25.556875 4.1104531 25.031234 4.0075625 24.505859 4.0078125 z M 24.498047 6.0097656 C 24.770047 6.0097656 25.0455 6.0647813 25.3125 6.1757812 L 45.041016 13.46875 L 24.794922 20.935547 C 24.551922 21.039547 24.266188 21.039828 23.992188 20.923828 L 4.9765625 13.5 L 23.705078 6.1738281 L 23.728516 6.1640625 C 23.972516 6.0610625 24.233047 6.0097656 24.498047 6.0097656 z M 1.9980469 17.001953 C 0.94204687 17.001953 -2.9605947e-16 17.853906 0 19.003906 L 0 34.970703 C 0 36.129703 0.95195312 36.972656 2.0019531 36.972656 C 2.2979531 36.972656 2.6015312 36.906766 2.8945312 36.759766 L 20.894531 27.998047 C 22.425531 27.232047 22.349531 25.022281 20.769531 24.363281 L 2.7695312 17.15625 C 2.5135313 17.05125 2.2520469 17.001953 1.9980469 17.001953 z M 48.001953 17.001953 C 47.760953 17.001953 47.514484 17.045625 47.271484 17.140625 L 28.271484 24.505859 C 27.505484 24.806859 27 25.544188 27 26.367188 L 27 42.998047 C 27 44.146047 27.940047 45 28.998047 45 C 29.239047 45 29.485516 44.956328 29.728516 44.861328 L 48.728516 37.425781 C 49.495516 37.125781 50 36.387453 50 35.564453 L 50 19.003906 C 50 17.855906 49.059953 17.001953 48.001953 17.001953 z M 2.0058594 19.003906 L 2.0253906 19.013672 L 20.017578 26.201172 L 2 34.970703 L 2.0058594 19.003906 z M 48 19.003906 L 48 35.564453 L 29 42.998047 L 28.994141 26.371094 L 48 19.003906 z"
/></svg
>
d="m228.6 47.2-190.9 79c-10.6 4.4-10.5 19.5.2 23.7l191.7 76c16.8 6.7 35.6 6.7 52.4 0l191.7-76c10.7-4.2 10.8-19.3.2-23.7L283 47.2c-17.4-7.2-37-7.2-54.4 0"
style="fill:#fcb400"
/><path
d="M272.8 267.5v189.9c0 9 9.1 15.2 17.5 11.9l213.6-82.9c4.9-1.9 8.1-6.6 8.1-11.9V184.6c0-9-9.1-15.2-17.5-11.9l-213.6 82.9c-4.9 1.9-8.1 6.6-8.1 11.9"
style="fill:#18bfff"
/><path
d="m222.9 277.3-63.4 30.6-6.4 3.1-133.8 64.1C10.8 379.2 0 373 0 363.6V185.4c0-3.4 1.7-6.4 4.1-8.6 1-1 2.1-1.8 3.2-2.4 3.2-1.9 7.8-2.4 11.6-.9l202.9 80.4c10.4 4.1 11.2 18.5 1.1 23.4"
style="fill:#f82b60"
/><path
d="m222.9 277.3-63.4 30.6L4.1 176.8c1-1 2.1-1.8 3.2-2.4 3.2-1.9 7.8-2.4 11.6-.9l202.9 80.4c10.4 4.1 11.2 18.5 1.1 23.4"
style="opacity:.29"
/>
</svg>
@@ -1,12 +1,22 @@
<script lang="ts">
interface Props {
height?: string;
width?: string;
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props();
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg {width} {height} viewBox="0 0 24 24" fill="#003DFF" xmlns="http://www.w3.org/2000/svg">
<path d="M12 0C5.445 0 .103 5.285.01 11.817c-.097 6.634 5.285 12.131 11.92 12.17a11.91 11.91 0 0 0 5.775-1.443.281.281 0 0 0 .052-.457l-1.122-.994a.79.79 0 0 0-.833-.14 9.693 9.693 0 0 1-3.923.77c-5.36-.067-9.692-4.527-9.607-9.888.084-5.293 4.417-9.573 9.73-9.573h9.73v17.296l-5.522-4.907a.407.407 0 0 0-.596.063 4.52 4.52 0 0 1-3.934 1.793 4.538 4.538 0 0 1-4.192-4.168 4.53 4.53 0 0 1 4.512-4.872 4.532 4.532 0 0 1 4.509 4.126c.018.205.11.397.265.533l1.438 1.275a.28.28 0 0 0 .462-.158 6.82 6.82 0 0 0 .099-1.725c-.232-3.376-2.966-6.092-6.345-6.3-3.873-.24-7.11 2.79-7.214 6.588-.1 3.7 2.933 6.892 6.634 6.974a6.75 6.75 0 0 0 4.136-1.294l7.212 6.394a.48.48 0 0 0 .797-.36V.456A.456.456 0 0 0 23.54 0Z"/>
<!-- #003DFF / #FFFFFF per algolia.com logo pack (Algolia-mark-blue.svg / Algolia-mark-white.svg). -->
<svg
class="text-[#003DFF] dark:text-[#FFFFFF]"
{width}
{height}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 0C5.445 0 .103 5.285.01 11.817c-.097 6.634 5.285 12.131 11.92 12.17a11.91 11.91 0 0 0 5.775-1.443.281.281 0 0 0 .052-.457l-1.122-.994a.79.79 0 0 0-.833-.14 9.693 9.693 0 0 1-3.923.77c-5.36-.067-9.692-4.527-9.607-9.888.084-5.293 4.417-9.573 9.73-9.573h9.73v17.296l-5.522-4.907a.407.407 0 0 0-.596.063 4.52 4.52 0 0 1-3.934 1.793 4.538 4.538 0 0 1-4.192-4.168 4.53 4.53 0 0 1 4.512-4.872 4.532 4.532 0 0 1 4.509 4.126c.018.205.11.397.265.533l1.438 1.275a.28.28 0 0 0 .462-.158 6.82 6.82 0 0 0 .099-1.725c-.232-3.376-2.966-6.092-6.345-6.3-3.873-.24-7.11 2.79-7.214 6.588-.1 3.7 2.933 6.892 6.634 6.974a6.75 6.75 0 0 0 4.136-1.294l7.212 6.394a.48.48 0 0 0 .797-.36V.456A.456.456 0 0 0 23.54 0Z"
/>
</svg>
@@ -8,6 +8,8 @@
let { size = 16, color = undefined, class: clazz = '' }: Props = $props()
</script>
<!-- No brand colour: AMQP is an OASIS protocol, not a vendor, and amqp.org publishes no palette
(https://www.amqp.org/legal.html). Generic glyph, deliberately monochrome — keep it on currentColor. -->
<svg
xmlns="http://www.w3.org/2000/svg"
width={`${size}px`}
@@ -7,7 +7,8 @@
let { height = '24px', width = '24px' }: Props = $props()
</script>
<!-- The mark is a near-black disc knocked out with a white "A", so it needs inverting
<!-- #000000 / #FFFFFF per ansible/logos community-marks (Black and White variants, CC BY-SA 4.0).
The mark is a solid disc knocked out with a white "A", so the pair is applied by inverting
rather than currentColor: recolouring the disc alone would leave white on light grey. -->
<svg
{height}
@@ -40,7 +41,7 @@
</g>
<path
d="m255.879 127.868c0 70.4551-57.1101 127.565-127.566 127.565-70.4501 0-127.566-57.1096-127.566-127.565 0-70.4501 57.1161-127.566 127.566-127.566 70.4561 0 127.566 57.1161 127.566 127.566"
fill="#1a1918"
fill="#000000"
/>
<path
d="m130.46 78.2289 33.0116 81.4763-49.8635-39.2778 16.8519-42.1984zm58.6445 100.245-50.7786-122.202c-1.44952-3.52436-4.34807-5.38926-7.86591-5.38926-3.52436 0-6.63386 1.86489-8.08339 5.38926l-55.7329 134.04h19.0653l22.0623-55.2653 65.8389 53.1899c2.64792 2.14114 4.55852 3.1095 7.0422 3.1095 4.97389 0 9.32146-3.72878 9.32146-9.11101 0-.87594-.30939-2.2662-.86941-3.76143z"
@@ -7,7 +7,15 @@
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg {width} {height} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<!-- #141413 / #FAF9F5 per anthropics/skills. -->
<svg
class="text-[#141413] dark:text-[#FAF9F5]"
{width}
{height}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z"
/>
@@ -0,0 +1,25 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
{width}
{height}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z"
/>
<circle cx="16.5" cy="7.5" r=".5" fill="currentColor" />
</svg>
@@ -1,22 +1,32 @@
<script lang="ts">
interface Props {
height?: string;
width?: string;
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props();
let { height = '24px', width = '24px' }: Props = $props()
</script>
<!-- #246DFF / #20A34E / #F86606 per apify.com/resources/brand. White/black variants are reserved for monochromatic contexts, so the tricolour mark stays in both themes. -->
<svg {width} {height} viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Apify logo icon (extracted from official SVG) -->
<g clip-path="url(#clip0_267_4154)">
<path d="M114.695 0H196.97C198.643 0 200 1.35671 200 3.03031V128.766C200 131.778 196.083 132.945 194.434 130.425L112.159 4.68953C110.841 2.67412 112.287 0 114.695 0Z" fill="#246DFF"/>
<path d="M85.3048 0H3.0303C1.35671 0 0 1.35671 0 3.03031V128.766C0 131.778 3.91698 132.945 5.566 130.425L87.8405 4.68953C89.1593 2.67412 87.7134 0 85.3048 0Z" fill="#20A34E"/>
<path d="M98.5909 100.668L5.12683 194.835C3.22886 196.747 4.58334 200 7.27759 200H192.8C195.483 200 196.842 196.77 194.967 194.852L102.908 100.685C101.726 99.4749 99.7824 99.4676 98.5909 100.668Z" fill="#F86606"/>
</g>
<defs>
<clipPath id="clip0_267_4154">
<rect width="200" height="200" fill="white"/>
</clipPath>
</defs>
<!-- Apify logo icon (extracted from official SVG) -->
<g clip-path="url(#clip0_267_4154)">
<path
d="M114.695 0H196.97C198.643 0 200 1.35671 200 3.03031V128.766C200 131.778 196.083 132.945 194.434 130.425L112.159 4.68953C110.841 2.67412 112.287 0 114.695 0Z"
fill="#246DFF"
/>
<path
d="M85.3048 0H3.0303C1.35671 0 0 1.35671 0 3.03031V128.766C0 131.778 3.91698 132.945 5.566 130.425L87.8405 4.68953C89.1593 2.67412 87.7134 0 85.3048 0Z"
fill="#20A34E"
/>
<path
d="M98.5909 100.668L5.12683 194.835C3.22886 196.747 4.58334 200 7.27759 200H192.8C195.483 200 196.842 196.77 194.967 194.852L102.908 100.685C101.726 99.4749 99.7824 99.4676 98.5909 100.668Z"
fill="#F86606"
/>
</g>
<defs>
<clipPath id="clip0_267_4154">
<rect width="200" height="200" fill="white" />
</clipPath>
</defs>
</svg>
@@ -1,12 +1,31 @@
<script lang="ts">
interface Props {
height?: string;
width?: string;
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props();
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg {width} {height} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M12,0C5.372,0 0,5.373 0,12 0,18.628 5.372,24 12,24 18.627,24 24,18.628 24,12A12.014,12.014 0 0 0 23.527,8.657 0.6,0.6 0 0 0 22.4,9.066H22.398C22.663,10.009 22.8,10.994 22.8,12A10.73,10.73 0 0 1 19.637,19.637 10.729,10.729 0 0 1 12,22.8 10.73,10.73 0 0 1 4.363,19.637 10.728,10.728 0 0 1 1.2,12 10.73,10.73 0 0 1 4.363,4.363 10.728,10.728 0 0 1 12,1.2C14.576,1.2 17.013,2.096 18.958,3.74A1.466,1.466 0 1 0 19.82,2.9 11.953,11.953 0 0 0 12,0ZM10.56,5.88 6.36,16.782H8.99L9.677,14.934H13.646L12.927,12.892H10.314L12.014,8.201 15.038,16.781H17.669L13.47,5.88Z"/>
<!-- #1F1F1E / #F8FF2C per apollo.io. -->
<svg
class="text-[#1F1F1E] dark:text-[#F8FF2C]"
{width}
{height}
viewBox="0 0 303 303"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M166.362 110.96L166.312 0.721829C161.444 0.251801 156.492 0 151.507 0C139.371 0 127.571 1.44366 116.257 4.12954L149.862 109.987C151.759 115.946 144.507 120.545 139.925 116.298L53.2608 36.1922C40.9738 46.6671 30.3821 59.0725 21.939 72.9551L132.438 131.407C147.831 139.548 166.362 128.385 166.362 110.96Z"
/>
<path
d="M136.272 192.56L136.322 302.245C141.324 302.748 146.393 303 151.513 303C163.464 303 175.097 301.607 186.259 298.988L152.772 193.534C150.875 187.575 158.126 182.975 162.709 187.222L249.272 267.228C261.609 256.803 272.251 244.431 280.745 230.599L170.195 172.131C154.803 163.989 136.272 175.153 136.272 192.577V192.56Z"
/>
<path
d="M186.777 140.371L267.045 53.5167C256.571 41.1785 244.167 30.5357 230.285 22.0752L171.67 132.901C163.529 148.295 174.691 166.827 192.115 166.81L302.245 166.76C302.748 161.741 303 156.654 303 151.501C303 139.515 301.607 127.865 298.972 116.685L193.088 150.309C187.129 152.206 182.53 144.954 186.777 140.371Z"
/>
<path
d="M110.516 136.711L0.721779 136.761C0.251783 141.612 0 146.531 0 151.5C0 163.62 1.42677 175.387 4.11246 186.685L109.526 153.212C115.485 151.315 120.084 158.567 115.837 163.15L36.005 249.534C46.4457 261.822 58.7998 272.431 72.6311 280.875L130.944 170.62C139.085 155.226 127.923 136.694 110.499 136.694L110.516 136.711Z"
/>
</svg>
@@ -1,21 +1,29 @@
<script lang="ts">
interface Props {
height?: string;
width?: string;
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props();
let { height = '24px', width = '24px' }: Props = $props()
</script>
<!-- #FD366E ("Appwrite Pink") per https://appwrite.io/assets. Brand asks that the logo not be altered, so no per-theme variant. -->
<svg
x="0px"
y="0px"
{width}
{height}
viewBox="0 0 168 168"
viewBox="0 30.3 512 451.5"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
style="fill:currentcolor;"
>
<path fill-rule="evenodd" clip-rule="evenodd" d="M111.716 139.591C101.618 139.591 92.1532 136.884 84.0067 132.157C77.8943 135.694 71.1023 138.062 63.9721 139.059C49.4512 141.09 34.7181 137.269 23.0142 128.437C17.2134 124.066 12.3308 118.594 8.64533 112.336C4.95993 106.077 2.54395 99.1532 1.53545 91.9602C0.527001 84.7672 0.94584 77.4461 2.768 70.4149C4.59016 63.3838 7.77996 56.7809 12.1551 50.9832C16.5302 45.1854 22.005 40.3066 28.2664 36.6256C34.5278 32.9446 41.4532 30.5335 48.6472 29.5301C55.8407 28.5267 63.1613 28.9507 70.1914 30.7777C75.0433 32.0389 79.6913 33.9515 84.0104 36.4554C92.1563 31.7292 101.62 29.023 111.716 29.023C142.249 29.023 167 53.7741 167 84.307C167 114.84 142.249 139.591 111.716 139.591ZM74.7533 43.1944L74.7699 43.1799C67.0929 39.7354 58.5382 38.4798 50.0717 39.6638C38.2317 41.3196 27.534 47.6111 20.3325 57.1542C13.1311 66.6976 10.0156 78.7102 11.6714 90.5507C13.3272 102.391 19.6187 113.088 29.1618 120.29C38.7048 127.491 50.718 130.607 62.558 128.951C74.3985 127.295 85.0961 121.003 92.2974 111.46C99.4987 101.917 102.614 89.9044 100.958 78.0639C100.194 72.6015 98.4441 67.3823 95.8425 62.6373C91.1307 54.0783 83.7183 47.2148 74.7533 43.1944Z"/>
<path d="M57.7961 65.1797C57.7297 65.3431 56.8847 68.6631 55.9743 72.6009C55.0296 76.5387 53.5325 82.7544 52.688 86.4318C51.0591 93.1683 50.0828 97.5943 50.0828 98.1805C50.0828 98.3418 51.0923 98.4736 52.328 98.4736H54.5747L55.5821 93.9812C56.1693 91.541 57.4714 85.9432 58.5125 81.5499C59.5542 77.156 60.8225 71.8191 61.3117 69.6705C61.7988 67.5223 62.287 65.5713 62.3855 65.3431C62.4836 65.0505 61.929 64.9535 60.2374 64.9535C58.9675 64.9535 57.8599 65.0505 57.7961 65.1797ZM40.3189 79.2715L37.3247 82.5267L38.2045 83.5673C38.6911 84.1524 40.0269 85.6179 41.1661 86.8224L43.2488 89.0344H49.1719L46.3738 86.0075C44.843 84.3822 43.5736 82.8192 43.5736 82.6242C43.5736 82.3965 44.7459 80.9325 46.1782 79.3701C47.6094 77.7765 48.7813 76.4075 48.7813 76.2451C48.7813 76.1149 47.5446 76.0174 46.048 76.0174H43.3469L40.3189 79.2715ZM63.1004 76.2119C63.1004 76.311 63.6539 76.9283 64.336 77.612C66.8763 80.1503 68.666 82.2995 68.568 82.7212C68.5037 82.95 67.2659 84.4782 65.7698 86.0734L63.0698 89.0344H66.0951L69.122 89.0022L71.8869 85.9764C73.4178 84.2852 74.654 82.786 74.654 82.591C74.654 82.4297 73.3524 80.8993 71.7246 79.1755L68.7967 76.0174H65.9639C64.3692 76.0174 63.1004 76.1149 63.1004 76.2119Z"/>
<style>
.appwrite-st0 {
fill: #fd366e;
}
</style><path
d="M512 368.9v112.9H225.2c-83.6 0-156.5-45.4-195.5-112.9-5.7-9.8-10.6-20.1-14.8-30.8C6.7 317.2 1.6 294.7 0 271.3v-30.5c.3-5.2.9-10.4 1.6-15.5C3 214.7 5.2 204.4 8 194.4 34.8 99.7 121.9 30.3 225.2 30.3s190.4 69.4 217.2 164.2H319.8c-20.1-30.9-55-51.3-94.6-51.3s-74.5 20.4-94.6 51.3c-6.1 9.4-10.9 19.7-14 30.8-2.8 9.8-4.2 20.1-4.2 30.8 0 32.4 13.6 61.5 35.4 82.1 20.2 19.1 47.5 30.8 77.4 30.8H512z"
class="appwrite-st0"
/><path
d="M512 225.2v112.9H302.7c21.8-20.6 35.4-49.7 35.4-82.1 0-10.7-1.5-21-4.2-30.8z"
class="appwrite-st0"
/>
</svg>
@@ -0,0 +1,15 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<!-- #006FDE per Esri's ArcGIS Pro product logo (esri.com/content/dam/esrisites/en-us/common/icons/product-logos/arcgis-pro-64.svg). Esri publishes no reversed variant for this badge and forbids altering its logos. -->
<svg {width} {height} viewBox="0 0 24 24" fill="#006FDE" xmlns="http://www.w3.org/2000/svg">
<path
d="M12 0a.84923.84923 0 0 0-.33766.07031l-8.5183 3.69444C2.1458 4.19776 1.4997 5.1816 1.4997 6.2697v13.2521l10.16264 4.40783c.21517.09333.46015.09407.67532.00073l8.5183-3.6959c.99824-.43301 1.64434-1.41685 1.64434-2.50495V4.47814L12.33766.06958C12.23007.02291 12.11516-.00005 12 0Zm0 4.83705c4.16294 0 7.53757 3.3746 7.53757 7.53757S16.163 19.91218 12 19.91218c-4.163 0-7.53757-3.37462-7.53757-7.53756S7.837 4.83705 12 4.83705zm-.3501 1.38871c-.89685-.02267-2.32742.2409-3.74645 1.6143.34958.55454.64544.97782.49 1.41801-.23127.65503-.5139.51378-1.07083.99466-.39567.34169.2067 1.01292-.31275 1.30595-.51945.29306-1.21315.6636-.94925 1.17557.2639.51196 1.4691.83013 1.95929 1.07522.49018.2451.92812.70605.6072 1.2371-.31403.51948-.53713 1.13083-.60134 1.60917 1.0549.94423 2.44706 1.51909 3.97423 1.51909 3.2928 0 5.81772-2.71048 5.96208-6.00017.04062-.92531-.93924-.93972-1.53447-.93972 0 0 .34061.92356.01831 1.43632-.3223.51278-.84968.76166-.83498 1.37699.01464.61533-.93743 1.5967-1.2598 1.9483-.32223.35163-.9228.74718-1.12796-.0586-.2051-.80579-.12596-1.47799.1084-2.04938.23442-.57136-.2174-.74707-.92068-.76174-.7032-.01463-1.0798-.10795-1.18656-1.19315-.08787-.89369 1.2429-1.84356 1.81426-1.84356.33406 0 1.45485.21963 1.50737-.34058.08056-.8593-.8204-1.04164-1.03934-1.60185C13.2877 7.58747 14.98596 6.60707 12 6.24993c-.10475-.01253-.22199-.02093-.3501-.02417z"
/>
</svg>
@@ -7,6 +7,7 @@
let { height = '24px', width = '24px' }: Props = $props()
</script>
<!-- #FF584A on both themes per asana.com/brand. Asana's guidelines forbid recolouring: the symbol always appears in coral, on light and dark backgrounds alike. -->
<svg
{width}
{height}
@@ -16,7 +17,7 @@
><title>Asana</title><g
><path
d="M200.324957,125.270044 C169.575962,125.270044 144.649915,150.19729 144.649915,180.947483 C144.649915,211.696478 169.575962,236.623724 200.324957,236.623724 C231.073952,236.623724 256,211.696478 256,180.947483 C256,150.19729 231.073952,125.270044 200.324957,125.270044 L200.324957,125.270044 Z M55.6754021,125.274837 C24.9270063,125.274837 0,150.19729 0,180.947483 C0,211.696478 24.9270063,236.623724 55.6754021,236.623724 C86.425116,236.623724 111.35332,211.696478 111.35332,180.947483 C111.35332,150.19729 86.425116,125.274837 55.6754021,125.274837 L55.6754021,125.274837 Z M183.674444,55.674204 C183.674444,86.425116 158.748396,111.354638 128.000599,111.354638 C97.2505258,111.354638 72.3247177,86.425116 72.3247177,55.674204 C72.3247177,24.9294026 97.2505258,0 128.000599,0 C158.748396,0 183.674444,24.9294026 183.674444,55.674204 L183.674444,55.674204 Z"
fill="#F06A6A"
fill="#FF584A"
/></g
></svg
>
@@ -0,0 +1,28 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<!-- #1D1B16 / #C7C3B2 per assemblyai.com (assemblyai-logo-full-primary.svg /
assemblyai-logo-full-secondary.svg). Two colours per theme, so the second stroke carries its
own fill- utilities: #777673 on light, #FFFFFF on dark. -->
<svg
class="text-[#1D1B16] dark:text-[#C7C3B2]"
{width}
{height}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.595 1.5a3.695 3.695 0 00-3.444 2.355L0 22.26h5.432l5.629-14.486h.002a.96.96 0 011.782 0h.75V4.835h-1.393L13.498 1.5h-2.902z"
/>
<path
class="fill-[#777673] dark:fill-[#FFFFFF]"
d="M7.151 3.855a3.695 3.695 0 013.26-2.35l-.002-.005H13.405c1.524 0 2.893.936 3.444 2.355L24 22.26h-5.525L11.54 4.413a2.528 2.528 0 00-4.609.006l.22-.564z"
/>
</svg>
@@ -6,7 +6,12 @@
class?: string
}
let { height = '24px', width = '24px', fill = 'black', class: className = '' }: Props = $props()
let {
height = '24px',
width = '24px',
fill = 'currentColor',
class: className = ''
}: Props = $props()
</script>
<svg
@@ -6,7 +6,12 @@
class?: string
}
let { height = '24px', width = '24px', fill = 'black', class: className = '' }: Props = $props()
let {
height = '24px',
width = '24px',
fill = 'currentColor',
class: className = ''
}: Props = $props()
</script>
<svg
@@ -0,0 +1,28 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<!-- #1C1D1F / #FFFFFF per the attio.com header logo (--color-black-100 / --color-white-100). The
mark is a filled compound path; stroking it instead thickens it and leaks the default black fill. -->
<svg
class="text-[#1C1D1F] dark:text-[#FFFFFF]"
{width}
{height}
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16.7802 11.5317L15.4723 9.43851C15.4723 9.43851 15.4674 9.42975 15.4645 9.42586L15.3613 9.2614C15.1667 8.94902 14.83 8.76218 14.4622 8.76121L12.3553 8.75439L12.2084 8.98989L9.69085 13.0187L9.55169 13.2415L10.6066 14.927C10.8012 15.2404 11.1379 15.4272 11.5087 15.4272H14.4612C14.8251 15.4272 15.1696 15.2355 15.3623 14.928L15.4664 14.7616C15.4664 14.7616 15.4703 14.7567 15.4713 14.7548L16.7812 12.6586C16.9962 12.3161 16.9962 11.8733 16.7812 11.5317H16.7802ZM16.3812 12.4085L15.0714 14.5047C15.0655 14.5144 15.0587 14.5222 15.0529 14.53C15.0071 14.5816 14.9478 14.5884 14.9215 14.5884C14.8913 14.5884 14.8174 14.5796 14.7697 14.5037L13.4598 12.4076C13.4452 12.3842 13.4326 12.3599 13.4209 12.3336C13.4092 12.3083 13.4005 12.283 13.3927 12.2567C13.3635 12.1516 13.3635 12.0387 13.3927 11.9336C13.4073 11.8821 13.4297 11.8305 13.4589 11.7838L14.7668 9.68958C14.7668 9.68958 14.7687 9.68666 14.7697 9.68472C14.8008 9.63801 14.8397 9.6166 14.8738 9.60979C14.8874 9.60589 14.8991 9.60492 14.9088 9.60297C14.9137 9.60297 14.9186 9.60297 14.9234 9.60297C14.9536 9.60297 15.0285 9.61271 15.0752 9.68861L16.3831 11.7818C16.5028 11.9726 16.5028 12.2178 16.3831 12.4085H16.3812Z"
fill="currentColor"
/>
<path
d="M12.9099 6.46677C13.124 6.12325 13.124 5.68145 12.9099 5.33988L11.602 3.24665L11.493 3.07051C11.2974 2.75813 10.9607 2.57129 10.5909 2.57129H7.63838C7.26956 2.57129 6.93285 2.75813 6.73628 3.07148L1.4492 11.5329C1.34313 11.7023 1.28571 11.8979 1.28571 12.0964C1.28571 12.2949 1.34216 12.4905 1.44823 12.6589L2.8661 14.9292C3.0617 15.2426 3.3984 15.4284 3.76722 15.4284H6.71974C7.0905 15.4284 7.42721 15.2416 7.62184 14.9282L7.72986 14.757C7.72986 14.757 7.72986 14.757 7.72986 14.755C7.72986 14.755 7.7318 14.7521 7.7318 14.7511L8.78572 13.0656L11.9095 8.06662L12.9079 6.46775L12.9099 6.46677ZM12.6014 5.90332C12.6014 6.01134 12.5712 6.12034 12.5099 6.21668L7.33087 14.5059C7.28416 14.5808 7.20923 14.5896 7.17906 14.5896C7.14889 14.5896 7.07493 14.5808 7.02725 14.5059L5.71837 12.4088C5.59965 12.219 5.59965 11.9748 5.71837 11.783L10.8974 3.49577C10.9441 3.41987 11.0191 3.41111 11.0492 3.41111C11.0794 3.41111 11.1543 3.41987 11.202 3.49675L12.5099 5.58997C12.5712 5.68631 12.6014 5.79531 12.6014 5.90332V5.90332Z"
fill="currentColor"
/>
</svg>
@@ -1,4 +1,5 @@
<script lang="ts">
import { twMerge } from 'tailwind-merge'
interface Props {
size?: number
height?: number
@@ -20,17 +21,27 @@
)
</script>
<!-- #232220 / #FFFFFF per auth0.com docs logo light.svg / dark.svg. Okta's content terms forbid altering the mark, so ship only these published variants. -->
<svg
role="img"
{width}
{height}
viewBox="0 0 256 285"
viewBox="0 0 19.7865 24"
fill={color ?? 'currentColor'}
xmlns="http://www.w3.org/2000/svg"
class={clazz}
class={twMerge('text-[#232220] dark:text-[#FFFFFF]', clazz)}
>
<title>auth0-svg</title>
<path
d="M220.412 0H127.997L156.559 89.006H248.975L174.205 142.083L202.775 231.594C250.903 196.534 266.629 143.474 248.983 89.006L220.412 0ZM7.01792 89.006H99.4339L127.997 0H35.5889L7.01792 89.006ZM7.01792 89.006C-10.6371 143.474 5.09792 196.535 53.2249 231.594L81.7879 142.084L7.01792 89.006ZM53.2259 231.594L127.996 284.564L202.766 231.594L127.996 177.747L53.2259 231.594Z"
d="M1.26834 10.2499C5.20746 9.60217 8.29463 6.51809 8.94341 2.58331L9.259 0.669875C9.31961 0.299735 9.01532 -0.0268029 8.64069 0.00174179C5.64144 0.23355 2.81491 1.2254 1.2539 1.86467C0.495844 2.17553 0 2.91142 0 3.73043V9.77877C0 10.1345 0.31842 10.4052 0.670128 10.3475L1.26834 10.2499Z"
/>
<path
d="M10.8431 2.58304C11.4915 6.51782 14.579 9.60158 18.5181 10.2496L19.1163 10.3475C19.4681 10.4052 19.7865 10.1345 19.7865 9.77881V3.73047C19.7865 2.91115 19.2906 2.17557 18.5326 1.86472C16.9716 1.22419 14.1438 0.233592 11.1458 0.00178354C10.7712 -0.0270748 10.4656 0.298523 10.5275 0.669918L10.8431 2.58304Z"
/>
<path
d="M18.5182 12.1463C14.5791 12.794 11.4919 15.8781 10.8431 19.8129L10.4497 23.5475C10.4136 23.8844 10.7885 24.1219 11.0708 23.9333C11.0736 23.9318 11.0752 23.9305 11.078 23.929C13.5485 22.2646 19.1839 17.9079 19.7416 12.4659C19.769 12.198 19.5313 11.9794 19.2659 12.0224L18.5192 12.1447L18.5182 12.1463Z"
/>
<path
d="M8.94451 19.8092C8.29605 15.8744 5.20857 12.7906 1.26945 12.1426L0.466486 12.0102C0.230026 11.9713 0.018374 12.167 0.0412978 12.406C0.570115 17.8785 6.28347 22.2624 8.77557 23.9296C9.0337 24.1009 9.37504 23.8951 9.34207 23.5871L8.94451 19.8092Z"
/>
</svg>
@@ -1,32 +1,48 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 335.000000 335.000000" width="24" height="24" >
<title>authelia-svg</title>
<g transform="translate(0.000000,335.000000) scale(0.100000,-0.100000)" fill="#3F51B4" stroke="none">
<path d="M1359 3300 c-142 -23 -312 -83 -477 -170 -422 -222 -751 -668 -836
<!-- #3F51B4 per authelia.com/images/branding/logo-cropped.svg (light stop of the official #3F51B4→#113155 gradient, flattened).
authelia.com/reference/guides/branding permits format/layout changes only — do not alter the design. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 335.000000 335.000000" width="24" height="24">
<title>authelia-svg</title>
<g
transform="translate(0.000000,335.000000) scale(0.100000,-0.100000)"
fill="#3F51B4"
stroke="none"
>
<path
d="M1359 3300 c-142 -23 -312 -83 -477 -170 -422 -222 -751 -668 -836
-1130 -31 -172 -36 -276 -21 -456 20 -235 69 -414 168 -610 56 -112 173 -280
251 -360 67 -70 74 -74 115 -74 57 0 111 49 111 103 0 36 -13 57 -87 141 -57
64 -156 212 -198 296 -50 99 -102 256 -126 383 -31 155 -31 388 0 534 94 447
385 818 795 1014 120 57 239 93 396 119 63 11 125 22 138 25 52 12 81 119 44
165 -15 19 -29 22 -117 25 -55 2 -125 0 -156 -5z"/>
<path d="M2049 2801 c-23 -24 -29 -38 -29 -73 0 -56 19 -82 83 -114 281 -142
165 -15 19 -29 22 -117 25 -55 2 -125 0 -156 -5z"
/>
<path
d="M2049 2801 c-23 -24 -29 -38 -29 -73 0 -56 19 -82 83 -114 281 -142
464 -357 552 -649 34 -113 61 -145 122 -145 33 0 50 7 75 29 30 27 33 34 31
83 -3 102 -83 298 -184 453 -64 98 -229 264 -324 326 -188 124 -269 147 -326
90z"/>
<path d="M1030 2750 c-113 -68 -175 -118 -263 -209 -315 -330 -418 -808 -271
90z"
/>
<path
d="M1030 2750 c-113 -68 -175 -118 -263 -209 -315 -330 -418 -808 -271
-1256 136 -417 487 -726 929 -821 98 -20 360 -24 454 -5 321 63 602 234 785
478 85 114 151 244 151 301 0 58 -45 102 -104 102 -55 0 -84 -27 -136 -129
-180 -348 -561 -567 -950 -548 -241 12 -474 110 -657 276 -130 118 -237 296
-285 477 -23 88 -26 119 -27 269 -1 148 2 181 22 255 28 101 93 242 152 331
59 88 191 215 279 269 41 25 92 62 114 83 37 35 39 40 34 80 -4 30 -15 50 -37
70 -47 42 -90 37 -190 -23z"/>
<path d="M1528 2494 c-312 -56 -570 -293 -652 -599 -24 -87 -31 -249 -16 -342
70 -47 42 -90 37 -190 -23z"
/>
<path
d="M1528 2494 c-312 -56 -570 -293 -652 -599 -24 -87 -31 -249 -16 -342
53 -330 298 -595 623 -673 158 -38 369 -22 512 40 177 76 329 219 415 391 187
371 69 825 -273 1053 -186 124 -396 169 -609 130z m216 -266 c25 -8 59 -31 86
-58 94 -94 94 -219 -1 -313 l-48 -49 64 -261 c72 -291 72 -297 12 -355 -43
-42 -105 -62 -191 -62 -115 0 -206 56 -221 137 -4 17 19 129 58 287 l65 258
-41 37 c-103 93 -103 245 1 332 67 56 135 71 216 47z"/>
<path d="M3034 2306 c-42 -42 -43 -69 -9 -159 50 -135 69 -256 69 -457 1 -206
-41 37 c-103 93 -103 245 1 332 67 56 135 71 216 47z"
/>
<path
d="M3034 2306 c-42 -42 -43 -69 -9 -159 50 -135 69 -256 69 -457 1 -206
-10 -279 -69 -457 -118 -354 -379 -654 -714 -823 -96 -48 -283 -106 -401 -125
-158 -25 -178 -32 -200 -68 -61 -99 20 -179 165 -162 702 79 1290 633 1421
1340 25 134 25 437 0 575 -26 150 -80 311 -114 343 -43 41 -103 38 -148 -7z"/>
</g>
1340 25 134 25 437 0 575 -26 150 -80 311 -114 343 -43 41 -103 38 -148 -7z"
/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -1,27 +1,19 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" width="24" height="24">
<title>authentik-svg</title>
<defs>
<style>.cls-1 {
fill: #fd4b2d;
}
</style>
</defs>
<rect class="cls-1" x="546.66" y="275.34" width="34.99" height="99.97"/>
<rect class="cls-1" x="637.66" y="271.13" width="34.99" height="78.19"/>
<path class="cls-1"
d="M127.64,385.31a127.57,127.57,0,0,0-112.13,66.9H74.82c26.27-22.67,64.42-29.28,92,0h62.8C205.11,419.06,168.36,385.31,127.64,385.31Z"/>
<path class="cls-1"
d="M212.39,512.53C130.55,683.65-12.89,537.81,74.82,452.21H15.51C-31,533.33,33.3,642.73,127.64,640.24c73,0,133.2-108.3,133.2-127.46,0-8.47-11.78-34.33-31.2-60.57h-62.8C187.65,471.08,205.81,498.56,212.39,512.53Zm2.17-5h0Z"/>
<path class="cls-1"
d="M999.94,274.11V725.89c0,86.58-70.42,157.06-157.05,157.06H776.22V729.12H457.88V883H391.22c-86.64,0-157.06-70.48-157.06-157.06V583.81H738.87V312.11H495.24V464.76H234.16V274.11a151.29,151.29,0,0,1,1.06-18,154.4,154.4,0,0,1,3.88-21.15c.58-2.23,1.23-4.46,1.88-6.64a13.66,13.66,0,0,1,.52-1.64c.36-1.12.71-2.17,1.06-3.23s.76-2.17,1.18-3.23c.47-1.23.88-2.41,1.35-3.58s1-2.35,1.47-3.53a159,159,0,0,1,14.27-26.49c.06-.06.12-.17.17-.23,1.41-2.06,2.88-4.11,4.41-6.17,1.29-1.7,2.58-3.35,3.88-5,1.52-1.82,3.11-3.7,4.69-5.46s3.12-3.47,4.76-5.11l.18-.18a36.53,36.53,0,0,1,2.64-2.64,159.75,159.75,0,0,1,18.68-15.63c1.76-1.29,3.64-2.52,5.52-3.76,2.11-1.35,4.23-2.64,6.4-3.93,4.11-2.41,8.28-4.64,12.63-6.64,1.35-.64,2.76-1.29,4.11-1.88a152.81,152.81,0,0,1,18.38-6.63c2.41-.71,4.82-1.35,7.29-1.94,1.17-.3,2.35-.59,3.58-.82a158.5,158.5,0,0,1,21.26-3.12l3.12-.17c.52,0,1-.06,1.52-.06,2.35-.12,4.76-.18,7.17-.18H842.89c2.4,0,4.81.06,7.16.18.53,0,1,.06,1.53.06l3.11.17A158.26,158.26,0,0,1,876,120.58c1.24.23,2.41.52,3.59.82,2.46.59,4.87,1.23,7.28,1.94A152.81,152.81,0,0,1,905.2,130c1.35.59,2.76,1.24,4.11,1.88,4.35,2,8.52,4.23,12.63,6.64,2.18,1.29,4.29,2.58,6.4,3.93,1.88,1.24,3.76,2.47,5.52,3.76a157.53,157.53,0,0,1,21.5,18.45c1.65,1.64,3.23,3.34,4.76,5.11s3.17,3.64,4.7,5.46c1.29,1.64,2.58,3.29,3.87,5,1.53,2.06,3,4.11,4.41,6.17.06.06.12.17.18.23a159.71,159.71,0,0,1,14.27,26.49c.47,1.18,1,2.35,1.47,3.53s.88,2.35,1.35,3.58c.41,1.06.82,2.11,1.17,3.23s.71,2.11,1.06,3.23a15.74,15.74,0,0,1,.53,1.64c.64,2.18,1.29,4.41,1.88,6.64a155.92,155.92,0,0,1,3.87,21.15A151.29,151.29,0,0,1,999.94,274.11Z"/>
<path class="cls-1"
d="M973.27,186.59H260.84A157.05,157.05,0,0,1,391.2,117.07H842.9A157.08,157.08,0,0,1,973.27,186.59Z"/>
<path class="cls-1"
d="M998.94,256.1H235.16a155.35,155.35,0,0,1,25.68-69.51H973.27A155.34,155.34,0,0,1,998.94,256.1Z"/>
<path class="cls-1"
d="M1000,274.11v51.51H738.87V312.11H495.24v13.51H234.1V274.11a153.41,153.41,0,0,1,1.06-18H998.94A151.29,151.29,0,0,1,1000,274.11Z"/>
<rect class="cls-1" x="234.1" y="325.62" width="261.13" height="69.54"/>
<rect class="cls-1" x="738.87" y="325.62" width="261.13" height="69.54"/>
<rect class="cls-1" x="234.1" y="395.16" width="261.13" height="69.48"/>
<rect class="cls-1" x="738.87" y="395.16" width="261.13" height="69.48"/>
<!-- #FD4B2D / #FFFFFF per goauthentik.io/press. The white variant is the brand's own asset for dark backgrounds; proportions and colour must not be altered otherwise. -->
<svg
id="Layer_1"
data-name="Layer 1"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1000 1000"
width="24"
height="24"
fill="currentColor"
class="text-[#FD4B2D] dark:text-[#FFFFFF]"
>
<title>authentik-svg</title>
<path
d="M830.2,118.09h-425.28c-93.1,0-169.27,76.17-169.27,169.27v425.28c0,93.1,76.17,169.27,169.27,169.27h50.18v-165.68h324.96v165.68h50.14c93.1,0,169.27-76.17,169.27-169.27v-425.28c0-93.1-76.17-169.27-169.27-169.27ZM756.51,581.62H235.93v-114.49h268.96v-158.97h43.68v94.7h25.61v-94.7h30.88v69.64h25.61v-69.64h30.88v116.35h25.61v-116.35h43.68v158.97h25.69v114.49Z"
/>
<path
d="M237.89,460.28h-.02c-25.34-34.27-63.32-69.15-105.42-69.15-48.4.03-92.89,26.58-115.91,69.15-48.08,83.85,18.39,196.94,115.91,194.36,75.46,0,137.69-111.95,137.69-131.75,0-8.76-12.18-35.49-32.25-62.61ZM77.85,460.28c27.16-23.43,66.59-30.27,95.1,0h.02c21.51,19.51,40.28,47.91,47.08,62.35-84.6,176.88-232.87,26.13-142.2-62.35Z"
/>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,12 +1,16 @@
<script lang="ts">
interface Props {
height?: string;
width?: string;
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props();
let { height = '24px', width = '24px' }: Props = $props()
</script>
<!-- #ED7100 tile with a #FFFFFF glyph per the AWS Architecture Icons package
(Icon-package_07312026, Arch_Containers/Arch_Amazon-Elastic-Container-Registry). AWS ships one flat
fill for both themes; the gradient tile was retired in the 2023 accessibility refresh. -->
<svg
x="0px"
y="0px"
@@ -17,7 +21,7 @@
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_305_193)">
<path d="M168 0H0V168H168V0Z" fill="url(#paint0_linear_305_193)" />
<path d="M168 0H0V168H168V0Z" fill="#ED7100" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
@@ -26,17 +30,6 @@
/>
</g>
<defs>
<linearGradient
id="paint0_linear_305_193"
x1="0"
y1="16800"
x2="16800"
y2="0"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="#C8511B" />
<stop offset="1" stop-color="#FF9900" />
</linearGradient>
<clipPath id="clip0_305_193">
<rect width="168" height="168" fill="white" />
</clipPath>
@@ -1,33 +1,38 @@
<!-- #252F3E / #FFFFFF wordmark, #FF9900 smile, per AWS's own logo files (d0.awsstatic.com/logos/powered-by-aws{,-white}.png).
aws.amazon.com/trademark-guidelines forbids altering the logo's colour, so only these two published variants may be used. -->
<script lang="ts">
import { twMerge } from 'tailwind-merge'
interface Props {
size?: number;
color?: string | undefined;
class?: string;
// `size` is what dynamic call sites pass (RowIcon, TriggersBadge, ToggleButton,
// global search); width/height stay for the ones that pass those instead.
size?: number
height?: string | number
width?: string | number
class?: string
}
let { size = 16, color = undefined, class: clazz = '' }: Props = $props();
let { size = 16, height = undefined, width = undefined, class: clazz = '' }: Props = $props()
</script>
<svg
width={size}
height={size}
viewBox="0 0 168 168"
class={clazz}
class={twMerge('text-[#252F3E] dark:text-white', clazz)}
width={width ?? size}
height={height ?? size}
viewBox="0.02 102.6 511.9 306.4"
xmlns="http://www.w3.org/2000/svg"
fill={color ?? 'currentColor'}
>
<path
d="M47.7805 70.052C47.7805 72.0948 48.0013 73.7511 48.3878 74.9657C48.8295 76.1803 49.3816 77.5054 50.1545 78.9408C50.4306 79.3825 50.541 79.8242 50.541 80.2106C50.541 80.7627 50.2097 81.3148 49.492 81.8669L46.0138 84.1858C45.5169 84.517 45.02 84.6826 44.5783 84.6826C44.0262 84.6826 43.4741 84.4066 42.922 83.9097C42.1491 83.0816 41.4866 82.1982 40.9345 81.3148C40.3824 80.3763 39.8303 79.3273 39.223 78.0575C34.9166 83.1368 29.506 85.6764 22.9913 85.6764C18.3536 85.6764 14.6546 84.3514 11.9493 81.7013C9.24402 79.0512 7.86377 75.5178 7.86377 71.101C7.86377 66.4082 9.52007 62.5987 12.8879 59.7278C16.2557 56.8569 20.7277 55.4214 26.4143 55.4214C28.2914 55.4214 30.2238 55.5871 32.2665 55.8631C34.3093 56.1392 36.4073 56.5808 38.6157 57.0777V53.0474C38.6157 48.8515 37.7323 45.9253 36.0208 44.2138C34.2541 42.5023 31.2727 41.6742 27.0216 41.6742C25.0892 41.6742 23.1017 41.895 21.0589 42.3919C19.0162 42.8888 17.0286 43.4961 15.0963 44.269C14.2129 44.6555 13.5504 44.8763 13.1639 44.9868C12.7774 45.0972 12.5014 45.1524 12.2806 45.1524C11.5076 45.1524 11.1212 44.6003 11.1212 43.4409V40.7356C11.1212 39.8522 11.2316 39.1897 11.5076 38.8033C11.7837 38.4168 12.2806 38.0303 13.0535 37.6439C14.9858 36.6501 17.3047 35.8219 20.0099 35.1594C22.7152 34.4417 25.5861 34.1104 28.6227 34.1104C35.1926 34.1104 39.9959 35.6011 43.0877 38.5824C46.1242 41.5637 47.6701 46.091 47.6701 52.164V70.052H47.7805ZM25.3653 78.4439C27.1872 78.4439 29.0644 78.1127 31.0519 77.4502C33.0395 76.7876 34.8062 75.573 36.2968 73.9167C37.1802 72.8677 37.8427 71.7083 38.174 70.3833C38.5052 69.0583 38.7261 67.4572 38.7261 65.58V63.2612C37.125 62.8748 35.4135 62.5435 33.6468 62.3227C31.8801 62.1018 30.1685 61.9914 28.457 61.9914C24.758 61.9914 22.0527 62.7091 20.2308 64.1998C18.4088 65.6905 17.5255 67.7884 17.5255 70.5489C17.5255 73.1438 18.188 75.0761 19.5683 76.4012C20.8933 77.7814 22.8256 78.4439 25.3653 78.4439ZM69.6988 84.4066C68.705 84.4066 68.0425 84.241 67.6008 83.8545C67.1591 83.5232 66.7727 82.7503 66.4414 81.7013L53.4671 39.0241C53.1358 37.9199 52.9702 37.2022 52.9702 36.8157C52.9702 35.9323 53.4119 35.4355 54.2952 35.4355H59.7058C60.7548 35.4355 61.4725 35.6011 61.859 35.9876C62.3007 36.3188 62.6319 37.0918 62.9632 38.1407L72.2384 74.6897L80.8512 38.1407C81.1272 37.0365 81.4585 36.3188 81.9002 35.9876C82.3418 35.6563 83.1148 35.4355 84.1086 35.4355H88.5253C89.5743 35.4355 90.2921 35.6011 90.7337 35.9876C91.1754 36.3188 91.5619 37.0918 91.7827 38.1407L100.506 75.1313L110.057 38.1407C110.388 37.0365 110.775 36.3188 111.161 35.9876C111.603 35.6563 112.321 35.4355 113.315 35.4355H118.449C119.332 35.4355 119.829 35.8771 119.829 36.8157C119.829 37.0918 119.774 37.3678 119.719 37.6991C119.664 38.0303 119.553 38.472 119.332 39.0793L106.027 81.7565C105.696 82.8607 105.309 83.5784 104.867 83.9097C104.426 84.241 103.708 84.4618 102.769 84.4618H98.0214C96.9725 84.4618 96.2547 84.2962 95.813 83.9097C95.3714 83.5232 94.9849 82.8055 94.7641 81.7013L86.2065 46.091L77.7042 81.6461C77.4282 82.7503 77.0969 83.468 76.6552 83.8545C76.2136 84.241 75.4406 84.4066 74.4468 84.4066H69.6988ZM140.643 85.8973C137.773 85.8973 134.902 85.566 132.141 84.9035C129.381 84.241 127.227 83.5232 125.792 82.6951C124.909 82.1982 124.301 81.6461 124.08 81.1492C123.86 80.6523 123.749 80.1002 123.749 79.6033V76.7876C123.749 75.6282 124.191 75.0761 125.019 75.0761C125.35 75.0761 125.682 75.1313 126.013 75.2418C126.344 75.3522 126.841 75.573 127.393 75.7939C129.27 76.622 131.313 77.2845 133.466 77.7262C135.675 78.1679 137.828 78.3887 140.036 78.3887C143.514 78.3887 146.22 77.7814 148.097 76.5668C149.974 75.3522 150.968 73.5855 150.968 71.3219C150.968 69.776 150.471 68.5062 149.477 67.4572C148.483 66.4082 146.606 65.4696 143.901 64.5863L135.895 62.1018C131.865 60.832 128.884 58.9549 127.062 56.4704C125.24 54.0412 124.301 51.3359 124.301 48.465C124.301 46.1462 124.798 44.1034 125.792 42.3367C126.786 40.57 128.111 39.0241 129.767 37.8095C131.423 36.5397 133.301 35.6011 135.509 34.9386C137.717 34.276 140.036 34 142.465 34C143.68 34 144.95 34.0552 146.164 34.2208C147.434 34.3865 148.594 34.6073 149.753 34.8281C150.857 35.1042 151.906 35.3802 152.9 35.7115C153.894 36.0428 154.667 36.374 155.219 36.7053C155.992 37.147 156.544 37.5886 156.875 38.0855C157.206 38.5272 157.372 39.1345 157.372 39.9075V42.5023C157.372 43.6617 156.93 44.269 156.102 44.269C155.661 44.269 154.943 44.0482 154.004 43.6065C150.857 42.1711 147.324 41.4533 143.404 41.4533C140.257 41.4533 137.773 41.9502 136.061 42.9992C134.35 44.0482 133.466 45.6493 133.466 47.9129C133.466 49.4588 134.018 50.7838 135.122 51.8328C136.227 52.8818 138.269 53.9308 141.196 54.8693L149.035 57.3538C153.01 58.6236 155.881 60.3903 157.593 62.6539C159.304 64.9175 160.133 67.5124 160.133 70.3833C160.133 72.7573 159.636 74.9105 158.697 76.7876C157.703 78.6648 156.378 80.3211 154.667 81.6461C152.955 83.0263 150.912 84.0201 148.538 84.7379C146.054 85.5108 143.459 85.8973 140.643 85.8973Z"
/>
<path
fill="currentColor"
d="M144.3 214.1c0 6.3.7 11.4 1.9 15.2 1.4 3.7 3.1 7.8 5.4 12.3.9 1.4 1.2 2.7 1.2 3.9 0 1.7-1 3.4-3.2 5.1l-10.7 7.2c-1.5 1-3.1 1.5-4.4 1.5-1.7 0-3.4-.9-5.1-2.4-2.4-2.6-4.4-5.3-6.1-8-1.7-2.9-3.4-6.1-5.3-10-13.3 15.7-30 23.5-50.1 23.5-14.3 0-25.7-4.1-34.1-12.3-8.3-8.2-12.6-19.1-12.6-32.7 0-14.5 5.1-26.2 15.5-35.1S60.8 169 78.4 169c5.8 0 11.7.5 18.1 1.4s12.8 2.2 19.6 3.7v-12.4c0-12.9-2.7-22-8-27.2-5.4-5.3-14.6-7.8-27.8-7.8-6 0-12.1.7-18.4 2.2s-12.4 3.4-18.4 5.8c-2.7 1.2-4.8 1.9-6 2.2s-2 .5-2.7.5c-2.4 0-3.6-1.7-3.6-5.3v-8.3c0-2.7.3-4.8 1.2-6s2.4-2.4 4.8-3.6c6-3.1 13.1-5.6 21.5-7.7 8.3-2.2 17.2-3.2 26.6-3.2 20.3 0 35.1 4.6 44.6 13.8 9.4 9.2 14.1 23.2 14.1 41.9v55.2h.3zM75.2 240c5.6 0 11.4-1 17.5-3.1 6.1-2 11.6-5.8 16.2-10.9 2.7-3.2 4.8-6.8 5.8-10.9s1.7-9 1.7-14.8v-7.2c-4.9-1.2-10.2-2.2-15.7-2.9-5.4-.7-10.7-1-16-1-11.4 0-19.8 2.2-25.4 6.8S51 207.1 51 215.6c0 8 2 14 6.3 18.1 4.1 4.2 10 6.3 17.9 6.3m136.7 18.4c-3.1 0-5.1-.5-6.5-1.7-1.4-1-2.6-3.4-3.6-6.6l-40-131.6c-1-3.4-1.5-5.6-1.5-6.8 0-2.7 1.4-4.3 4.1-4.3h16.7c3.2 0 5.4.5 6.6 1.7 1.4 1 2.4 3.4 3.4 6.6l28.6 112.7 26.6-112.7c.9-3.4 1.9-5.6 3.2-6.6 1.4-1 3.7-1.7 6.8-1.7H270c3.2 0 5.4.5 6.8 1.7 1.4 1 2.6 3.4 3.2 6.6l26.9 114.1 29.5-114.1c1-3.4 2.2-5.6 3.4-6.6 1.4-1 3.6-1.7 6.6-1.7h15.8c2.7 0 4.3 1.4 4.3 4.3 0 .9-.2 1.7-.3 2.7-.2 1-.5 2.4-1.2 4.3l-41 131.6q-1.5 5.1-3.6 6.6c-1.4 1-3.6 1.7-6.5 1.7h-14.6c-3.2 0-5.4-.5-6.8-1.7s-2.6-3.4-3.2-6.8l-26.4-109.8L236.7 250c-.9 3.4-1.9 5.6-3.2 6.8-1.4 1.2-3.7 1.7-6.8 1.7h-14.8zm218.8 4.6c-8.9 0-17.7-1-26.2-3.1-8.5-2-15.2-4.3-19.6-6.8-2.7-1.5-4.6-3.2-5.3-4.8s-1-3.2-1-4.8v-8.7c0-3.6 1.4-5.3 3.9-5.3 1 0 2 .2 3.1.5 1 .3 2.6 1 4.3 1.7 5.8 2.6 12.1 4.6 18.7 6 6.8 1.4 13.5 2 20.3 2 10.7 0 19.1-1.9 24.9-5.6s8.9-9.2 8.9-16.2c0-4.8-1.5-8.7-4.6-11.9s-8.9-6.1-17.2-8.9l-24.7-7.7c-12.4-3.9-21.6-9.7-27.2-17.4-5.6-7.5-8.5-15.8-8.5-24.7 0-7.2 1.5-13.5 4.6-18.9s7.2-10.2 12.3-14c5.1-3.9 10.9-6.8 17.7-8.9 6.8-2 14-2.9 21.5-2.9 3.7 0 7.7.2 11.4.7 3.9.5 7.5 1.2 11.1 1.9 3.4.9 6.6 1.7 9.7 2.7s5.4 2 7.2 3.1c2.4 1.4 4.1 2.7 5.1 4.3 1 1.4 1.5 3.2 1.5 5.6v8c0 3.6-1.4 5.4-3.9 5.4-1.4 0-3.6-.7-6.5-2q-14.55-6.6-32.7-6.6c-9.7 0-17.4 1.5-22.6 4.8s-8 8.2-8 15.2c0 4.8 1.7 8.9 5.1 12.1s9.7 6.5 18.7 9.4l24.2 7.7c12.3 3.9 21.1 9.4 26.4 16.3s7.8 15 7.8 23.8c0 7.3-1.5 14-4.4 19.8-3.1 5.8-7.2 10.9-12.4 15-5.3 4.3-11.6 7.3-18.9 9.5-8 2.5-16 3.7-24.7 3.7"
/><path
fill="#FF9900"
fill-rule="evenodd"
clip-rule="evenodd"
d="M151.078 112.729C132.914 126.145 106.524 133.267 83.8324 133.267C52.0316 133.267 23.3777 121.508 1.7354 101.963C0.0238993 100.417 1.56977 98.3195 3.61254 99.5341C27.0215 113.116 55.8963 121.342 85.7648 121.342C105.916 121.342 128.056 117.146 148.428 108.533C151.465 107.153 154.059 110.521 151.078 112.729Z"
/>
<path
d="M462.9 345.7c-56 41.4-137.4 63.3-207.4 63.3-98.1 0-186.5-36.3-253.2-96.6-5.3-4.8-.5-11.2 5.8-7.5 72.2 41.9 161.3 67.3 253.4 67.3 62.2 0 130.4-12.9 193.3-39.5 9.3-4.2 17.3 6.2 8.1 13"
/><path
fill="#FF9900"
fill-rule="evenodd"
clip-rule="evenodd"
d="M158.642 104.117C156.323 101.135 143.294 102.681 137.386 103.399C135.62 103.62 135.343 102.074 136.945 100.914C147.324 93.6267 164.384 95.7246 166.371 98.1539C168.359 100.638 165.819 117.698 156.102 125.869C154.612 127.139 153.176 126.477 153.839 124.82C156.047 119.354 160.961 107.043 158.642 104.117Z"
d="M486.2 319.2c-7.2-9.2-47.3-4.4-65.6-2.2-5.4.7-6.3-4.1-1.4-7.7 32-22.5 84.6-16 90.8-8.5 6.1 7.7-1.7 60.3-31.7 85.5-4.6 3.9-9 1.9-7-3.2 6.9-16.9 22.1-54.9 14.9-63.9"
/>
</svg>
@@ -1,22 +1,86 @@
<script lang="ts">
import { twMerge } from 'tailwind-merge'
interface Props {
size?: number
color?: string | undefined
class?: string
}
let { size = 16, color = undefined, class: clazz = '' }: Props = $props()
let { size = 16, class: clazz = '' }: Props = $props()
</script>
<!-- Microsoft's own logo_azure.svg (learn.microsoft.com/media/logos/logo_azure.svg): #0078d4 with
the #114a8b->#0669bc and #3ccbf4->#2892df gradients. Shipped unmodified apart from namespacing
the gradient ids, which are document-global. Microsoft's icon terms say to use the icons "as they
would appear within Azure" and forbid changing them, so it must not be recoloured or drawn
monochrome; their permitted use is diagrams, training and documentation
(learn.microsoft.com/azure/architecture/icons). -->
<svg
xmlns="http://www.w3.org/2000/svg"
width={`${size}px`}
height={`${size}px`}
viewBox="0 0 24 24"
fill={color ?? 'currentColor'}
class={clazz}
viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-label="Azure"
class={twMerge(clazz)}
>
<path
d="M22.379 23.343a1.62 1.62 0 0 0 1.536-1.1l.029-.092q.053-.164.081-.336v-.016a1.68 1.68 0 0 0-.268-1.227L15.147 5.44a1.63 1.63 0 0 0-1.354-.724l-3.473.011L5.94 8.57a6 6 0 0 0-1.386 1.74L.262 17.717a1.63 1.63 0 0 0 1.422 2.429l-.055-.013zM13.398 7.25l5.322 9.183-10.683.024zm-3.363 12.754-8.316.021 8.318-14.4 1.795 3.1-6.516 11.274z"
/>
<defs>
<linearGradient
id="azure-grad-2"
x1="-960.6062"
y1="283.3968"
x2="-1032.5112"
y2="70.9723"
gradientTransform="matrix(1, 0, 0, -1, 1075, 318)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stop-color="#114a8b" />
<stop offset="1" stop-color="#0669bc" />
</linearGradient>
<linearGradient
id="azure-grad-3"
x1="-938.1444"
y1="184.4016"
x2="-954.7776"
y2="178.7775"
gradientTransform="matrix(1, 0, 0, -1, 1075, 318)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stop-opacity="0.3" />
<stop offset="0.0712" stop-opacity="0.2" />
<stop offset="0.321" stop-opacity="0.1" />
<stop offset="0.6231" stop-opacity="0.05" />
<stop offset="1" stop-opacity="0" />
</linearGradient>
<linearGradient
id="azure-grad-1"
x1="-947.2919"
y1="289.5941"
x2="-868.3628"
y2="79.3082"
gradientTransform="matrix(1, 0, 0, -1, 1075, 318)"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" stop-color="#3ccbf4" />
<stop offset="1" stop-color="#2892df" />
</linearGradient>
</defs>
<g>
<path
d="M89.1583,18.266h69.238L86.5228,231.2237a11.0411,11.0411,0,0,1-10.4612,7.51H22.1787a11.0229,11.0229,0,0,1-10.4446-14.5479l66.9633-198.41A11.0406,11.0406,0,0,1,89.1583,18.266Z"
fill="url(#azure-grad-2)"
/>
<path
d="M189.7708,161.1044H79.9752a5.0829,5.0829,0,0,0-3.4681,8.7988l70.5517,65.8479a11.0915,11.0915,0,0,0,7.5668,2.9829h62.1675Z"
fill="#0078d4"
/>
<path
d="M89.1583,18.266A10.95,10.95,0,0,0,78.675,25.92L11.8168,224.0061A11.0094,11.0094,0,0,0,22.21,238.734H77.4839a11.8143,11.8143,0,0,0,9.0688-7.7138L99.8838,191.73l47.6243,44.4181a11.2671,11.2671,0,0,0,7.0889,2.5863h61.9371l-27.1656-77.63-79.1905.0176,48.47-142.856Z"
fill="url(#azure-grad-3)"
/>
<path
d="M177.592,25.7643a11.0227,11.0227,0,0,0-10.4439-7.4983H89.9841a11.0245,11.0245,0,0,1,10.445,7.4983l66.967,198.4209a11.0245,11.0245,0,0,1-10.445,14.5488h77.164a11.0235,11.0235,0,0,0,10.444-14.5488Z"
fill="url(#azure-grad-1)"
/>
</g>
</svg>
@@ -0,0 +1,578 @@
# Icon brand colours
Where every icon's colours come from, and whether they survive both app surfaces.
Compiled from the components themselves during the audit — colours read from the fills and
the Tailwind pair classes, sources from each component's provenance comment, contrast
computed from those hexes against `surface-primary` in each theme. Maintained by hand from
here on: change an icon's colour or source and change its row.
Surfaces: light `#fbfbfd`, dark `#2e3441`. Ratios are WCAG non-text contrast; **bold** marks a
mark that is effectively invisible on that surface. WCAG exempts logotypes from the 3:1
floor, so a low ratio is a signal the colour may be wrong, not automatically a defect.
`pair` = brand publishes a per-theme variant. Usually applied as `text-[#light] dark:text-[#dark]`; `AnsibleIcon` inverts instead (`dark:invert`), and `DatadogIcon`, `DenoIcon`, `DeepLIcon` and `TogglIcon` swap between two SVGs (`dark:hidden` / `hidden dark:block`) because their two marks are different artwork, not the same shape recoloured.
`fixed` = full-colour mark, same in both themes. `inherits` = brand publishes no colour,
so the mark takes the surrounding text colour. `mixed` = the root carries a
`fill="currentColor"` that hardcoded path fills override, so it is inert — these are
candidates for cleanup, not theme-aware icons.
The Light/Dark columns show the colour that carries the mark; white and black knockout
details are omitted. Ratios are the best contrast any part of the mark achieves.
**Do not change a colour here without a first-party source.** Several of these look like
mistakes and are not: Cal.com is deliberately greyscale, Google Cloud may not be recoloured,
Stripe is blurple rather than black. Third-party icon sets go stale and have been wrong
repeatedly — check the brand's own page.
| Icon | Resource types | Mode | Light | Dark | ☀ | 🌙 | Source |
|---|---|---|---|---|---|---|---|
| `AblyIcon` | `ably` | fixed | #FF5416 | #FF5416 | 3.87 | 3.88 | brand.ably.com/logo |
| `AbstractApiIcon` | `abstractapi` | fixed | #20E492 | #20E492 | **1.62** | 12.47 | abstractapi.com's own logo SVG (6538df34291c9fa4ed28d6f7_Logo.svg) |
| `AcceloIcon` | `accelo` | fixed | #4C49CB | #4C49CB | 6.51 | 8.15 | Accelo_Logo-Primary.svg on accelo.com |
| `ActiveCampaignIcon` | `activecampaign` | pair | #004CFF | #FFFFFF | 5.84 | 12.47 | activecampaign.com/brand logo pack (ActiveCampaign-Glyph-Blue.svg / ActiveCampaign-Glyph-White.svg) |
| `ActivitypubIcon` | `activitypub` | fixed | #F1007E | #F1007E | 5.01 | 2.99 | activitypub.rocks/static/images/ActivityPub-logo.svg |
| `AcumbamailIcon` | `acumbamail` | fixed | #E62F71 | #E62F71 | 8.83 | 8.86 | Acumbamail's own isotype SVG, /static/favico/Acumbamail/favicon-32.svg on acumbamail.com |
| `AdhookIcon` | `adhook` | fixed | #00ACC6 | #00ACC6 | 2.63 | 4.58 | adhook's own logo (https://adhook.io/fr/images/logo.svg, `.cls-1{fill:#00acc6}`) |
| `AdobeAcrobatSignIcon` | `adobe_acrobat_sign` | fixed | #584CCC | #584CCC | 6.12 | 12.47 | Adobe's own Acrobat Sign product icon (adobe.com/cc-shared/assets/img/product-icons/svg/acrobat-sign.svg); same value in the live app favicon |
| `Ai21Icon` | `ai21` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | ai21.com (ai21-logo-black.svg / ai21-logo-white.svg) |
| `AirtableIcon` | `airtable` | mixed | #FCB400 | #FCB400 | 3.88 | 6.93 | airtable.com/favicon.ico (fixed full-colour mark: #18BFFF and #F82B60 panels) |
| `AlgoliaIcon` | `algolia` | pair | #003DFF | #FFFFFF | 6.53 | 12.47 | algolia.com logo pack (Algolia-mark-blue.svg / Algolia-mark-white.svg) |
| `AmqpIcon` | `amqp` | fixed | — | — | — | — | — |
| `AnsibleIcon` | `ansible` | pair | #1A1918 | #E5E6E7 | 16.99 | 9.98 | ansible/logos community-marks (Black and White variants, CC BY-SA 4.0) |
| `AnthropicIcon` | `anthropic` | pair | #141413 | #FAF9F5 | 17.84 | 11.84 | anthropics/skills |
| `ApifyIcon` | `apify` | fixed | #246DFF | #246DFF | 4.32 | 12.47 | apify.com/resources/brand |
| `ApolloIcon` | `apollo` | pair | #1F1F1E | #F8FF2C | 15.96 | 11.48 | apollo.io |
| `AppwriteIcon` | `appwrite` | mixed | #FD366E | #FD366E | 3.88 | 5.71 | https://appwrite.io/assets |
| `ArcGisIcon` | `arcgis_account` | fixed | #006FDE | #006FDE | 4.69 | 2.57 | Esri's ArcGIS Pro product logo (esri.com/content/dam/esrisites/en-us/common/icons/product-logos/arcgis-pro-64.svg) |
| `AsanaIcon` | `asana` | fixed | #FF584A | #FF584A | 3.01 | 4.01 | asana.com/brand |
| `AssemblyAiIcon` | `assemblyai` | pair | #1D1B16 | #C7C3B2 | 16.65 | 12.47 | assemblyai.com (assemblyai-logo-full-primary.svg / assemblyai-logo-full-secondary.svg) |
| `AttioIcon` | `attio` | pair | #1C1D1F | #FFFFFF | 16.32 | 12.47 | the attio.com header logo (--color-black-100 / --color-white-100) |
| `Auth0Icon` | `auth0` | pair | #232220 | #FFFFFF | 15.38 | 12.47 | auth0.com docs logo light.svg / dark.svg |
| `AutheliaIcon` | `authelia` | fixed | #3F51B4 | #3F51B4 | 6.67 | 1.81 | authelia.com/images/branding/logo-cropped.svg (light stop of the official #3F51B4#113155 gradient, flattened) |
| `AuthentikIcon` | `authentik` | pair | #FD4B2D | #FFFFFF | 3.27 | 12.47 | goauthentik.io/press |
| `AwsEcrIcon` | `aws_ecr` | fixed | #ED7100 | #ED7100 | 2.92 | 12.47 | the AWS Architecture Icons package (Icon-package_07312026, Arch_Containers/Arch_Amazon-Elastic-Container-Registry) |
| `AwsIcon` | `aws`, `redshift` | pair | #252F3E | #FF9900 | 13.07 | 5.83 | AWS's own logo files (d0.awsstatic.com/logos/powered-by-aws{,-white}.png) |
| `AzureIcon` | `azure` | fixed | — | — | — | — | Microsoft's own logo_azure.svg (learn.microsoft.com/media/logos/logo_azure.svg), whose outer wedges add the #114A8B->#0669BC and #3CCBF4->#2892DF gradients |
| `BambooHrIcon` | `bamboo_hr` | pair | #599D15 | #FFFFFF | 3.25 | 12.47 | bamboohr.com (Encore --brandColor; bamboohr-logo-white.png is the published reversed variant) |
| `BaremetricsIcon` | `baremetrics` | fixed | #5386FF | #5386FF | 3.27 | 3.70 | the mark in baremetrics.com's header logo (baremetrics-logo.svg), the asset this path is taken from |
| `BaserowIcon` | `baserow`, `baserow_table` | fixed | #2BC3F1 | #2BC3F1 | 4.96 | 6.05 | the baserow.io favicon and horizontal logo |
| `BasisTheoryIcon` | `basis_theory` | pair | #1D2032 | #EBEDFF | 15.57 | 10.74 | developers.basistheory.com/img/bt-logo-light.svg and bt-logo-dark.svg, which ship the same mark geometry in the two theme colours |
| `BeamerIcon` | `beamer` | pair | #1C1E21 | #FFFFFF | 16.16 | 12.47 | the getbeamer.com header logo (g#isotype) and their webclip app icon, which sets the same mark in white on #1C1E21 |
| `BigQueryIcon` | `bigquery` | fixed | #34A853 | #34A853 | 3.80 | 7.30 | Google Cloud's official icon library (cloud.google.com/icons, core-products-icons.zip) |
| `BitbucketIcon` | `bitbucket` | pair | #1868DB | #FFFFFF | 5.03 | 12.47 | atlassian.design/foundations/logos (Bitbucket mark, brand and inverse) |
| `BitlyIcon` | `bitly` | fixed | #F36600 | #F36600 | 3.03 | 3.99 | bitly.com/pages/bitly-logo-usage-guidelines-for-media (Bitly-MediaKit glyph_bitly_orange_RGB.svg) |
| `BloggerIcon` | `blogger` | fixed | #F57C00 | #F57C00 | 2.62 | 12.47 | Google's Blogger product logo (gstatic.com/images/branding/productlogos/blogger/v5/192px.svg) |
| `BlueskyIcon` | `bluesky` | pair | #0560FF | #FFFFFF | 4.93 | 12.47 | bsky.social/about/support/branding |
| `BotifyIcon` | `botify` | fixed | #A973FF | #A973FF | 3.09 | 3.91 | botify.com design tokens (--color--surface--purple-05) |
| `BoxIcon` | `box` | pair | #0061D5 | #FFFFFF | 5.54 | 12.47 | box.com (.box-logo-svg fill:#0061d5, reversed to #fff over the dark masthead) |
| `BrevoIcon` | `brevo`, `sendinblue` | fixed | #0B996E | #0B996E | 3.51 | 12.47 | brevo.com's favicon.svg |
| `BrexIcon` | `brex` | pair | #15191E | #FFFFFF | 17.08 | 12.47 | brex.com |
| `BrowserlessIcon` | `browserless` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | browserless.io/favicon.svg |
| `BubbleIcon` | `bubble` | mixed | #0000FF | #0000FF | 8.31 | 5.71 | the logo SVG served on bubble.io/brand; the B is #262626 there, kept as currentColor so the monochrome part follows the app theme |
| `BuildkiteIcon` | `buildkite` | fixed | #30F2A2 | #30F2A2 | 2.04 | 8.52 | buildkite.com/about/brand-assets |
| `BunIcon` | — | fixed | #FBF0DF | #FBF0DF | 6.48 | 12.47 | https://bun.com/logo.svg |
| `ButtondownIcon` | `buttondown` | fixed | #0069FF | #0069FF | 4.55 | 2.65 | https://buttondown.com/brand |
| `CSharpIcon` | — | fixed | #927BE5 | #927BE5 | 7.68 | 12.47 | dotnet/brand logo/language-icons/csharp-72.svg (CC0) |
| `CalcomIcon` | `calcom` | pair | #292929 | #FAFAFA | 14.08 | 11.95 | design.cal.com |
| `CalendlyIcon` | `calendly` | pair | #006BFF | #FFFFFF | 4.47 | 12.47 | Calendly's 2024 External Brand Guidelines and calendly_brand mark_white.svg (media kit on calendly.com/newsroom) |
| `CampaynIcon` | `campayn` | fixed | #008AFF | #008AFF | 3.34 | 12.47 | app.campayn.com/images/campayn/favicons/safari-pinned-tab.svg (colours sampled from android-chrome-512x512.png in the same directory) |
| `CertopusIcon` | `certopus` | fixed | #FF6E30 | #FF6E30 | 12.07 | 12.47 | https://certopus.com/images/logo/logo_circle.svg |
| `ChromaIcon` | `chromadb` | fixed | #FFDE2D | #FFDE2D | 3.65 | 9.35 | Chroma's own logo SVG served by trychroma.com (chroma-wordmark.svg) |
| `CircleCiIcon` | `circleci` | pair | #161616 | #FFFFFF | 17.51 | 12.47 | brand.circleci.com |
| `CiscoIcon` | `cisco` | pair | #00BCEB | #FFFFFF | 2.16 | 12.47 | cisco.com logo SVG and newsroom.cisco.com/logos |
| `ClaudeIcon` | — | fixed | #D97757 | #D97757 | 3.02 | 12.47 | https://claude.ai/favicon.svg (Anthropic's own asset) |
| `ClearbitIcon` | `clearbit` | fixed | #4DB1FD | #4DB1FD | 20.32 | 10.83 | clearbit.com/logo.svg |
| `ClerkIcon` | `clerk` | fixed | #BAB1FF | #BAB1FF | 5.10 | 6.43 | clerk.com/brand-assets (symbol-primary.svg) |
| `ClickhouseIcon` | `clickhouse` | pair | #161616 | #FFFFFF | 17.51 | 12.47 | clickhouse.design/brand/logo-usage (logomark, on-light / on-dark) |
| `ClickupIcon` | `clickup` | fixed | #6647F0 | #6647F0 | 5.46 | 4.12 | clickup.com/brand (v4 Logomark-gradient.svg); the gradient mark is the same on light and dark, and the guidelines say "don't change the color" |
| `CloseIcon` | `close` | fixed | #4EC375 | #4EC375 | 4.77 | 7.39 | close.com/brand (close-logo-2024 mark.svg) |
| `CloudflareIcon` | `cloudflare` | fixed | #FF5F08 | #FF5F08 | 2.95 | 5.83 | the logomark shipped on cloudflare.com, blog.cloudflare.com and workers.cloudflare.com |
| `CloudinaryIcon` | `cloudinary` | pair | #3448C5 | #FFFFFF | 7.06 | 12.47 | cloudinary_logo_for_white_bg.svg and cloudinary_logo_for_black_bg.svg on cloudinary-res.cloudinary.com |
| `CockroachDbIcon` | `cockroachdb` | pair | #6933FF | #FFFFFF | 5.78 | 12.47 | cockroachlabs.com (electric-purple-500, also the CockroachDB docs primaryColor) and the docs light/dark logo pair |
| `CodaIcon` | `coda` | fixed | #F46A54 | #F46A54 | 2.89 | 4.18 | Coda's own app icon, https://cdn.coda.io/icons/png/color/coda-192.png (single-colour mark, no dark variant published) |
| `CodatIcon` | `codat` | fixed | #D1E100 | #D1E100 | 17.31 | 8.60 | codat.io (logo-white.svg glyph outlines, colours from the site palette); framing matches their 300x300 favicon exactly |
| `CohereIcon` | `cohere` | fixed | #355146 | #355146 | 8.41 | 12.47 | https://cohere.com/logo.svg |
| `CoinMarketCapIcon` | `coinmarketcap` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | coinmarketcap.com |
| `CoinbaseIcon` | `coinbase` | pair | #0052FF | — | 5.57 | — | Coinbase's own light/dark logo files (mintcdn.com/coinbase-prod/.../logos/wordmark-light.svg and wordmark-dark.svg, served by docs.cdp.coinbase.com) |
| `ComapeoIcon` | `comapeo_server` | pair | #022199 | #0066FF | 12.09 | 2.58 | the CoMapeo Cloud mark shipped as public/favicon.svg in digidem/comapeo-cloud-app (the server this resource connects to) |
| `ConfluenceIcon` | `confluence` | fixed | #1868DB | #1868DB | 5.03 | 12.47 | Atlassian's @atlaskit/logo (atlassian.design logo library) |
| `ContentfulIcon` | `contentful` | fixed | #1773EB | #1773EB | 4.33 | 9.08 | Contentful's Forma 36 design system (ContentfulLogoIcon) |
| `ContiguityIcon` | `contiguity` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | contiguity.com/assets/icon-white.png and icon-black.png (docs.contiguity.com likewise ships logo/black.svg for light and logo/white.svg for dark) |
| `ConvertKitIcon` | `convertkit` | pair | #1E1E1E | #F2EFE9 | 16.13 | 10.87 | kit.com/brand |
| `CoupaIcon` | `coupa` | pair | #1565C0 | #FFFFFF | 5.56 | 12.47 | the Coupa logo kit linked from coupa.com/company/press-kit, which ships the mark in blue and a white reversed variant |
| `CssIcon` | — | fixed | #663399 | #663399 | 8.13 | 12.47 | github.com/CSS-Next/logo.css (CC0), the official CSS logo endorsed by the W3C CSS WG |
| `CurrencyApiIcon` | `currencyapi` | fixed | #2994FF | #2994FF | 9.13 | 4.67 | currencyapi.com/img/currencyapi_logo_color.svg |
| `DatabricksIcon` | `databricks` | fixed | #FF3621 | #FF3621 | 3.50 | 3.45 | Databricks' own logo asset (databricks.com/sites/default/files/2023-08/databricks-default.png) |
| `DatadogIcon` | `datadog` | pair | #632CA6 | #FFFFFF | 8.32 | 12.47 | datadoghq.com press kit |
| `DatoCmsIcon` | `datocms` | fixed | #FF7751 | #FF7751 | 2.54 | 4.76 | datocms.com/company/brand-assets |
| `DbtIcon` | `dbt_profile` | fixed | #FE6703 | #FE6703 | 2.84 | 4.25 | the dbt Labs brand assets (getdbt.com/brand-guidelines) |
| `DeelIcon` | `deel` | pair | #1B1B1B | #FFFFFF | 16.67 | 12.47 | deel.com's own logo_revamp.svg / logo_revamp_white.svg |
| `DeepInfraIcon` | `deep_infra` | pair | #2A3275 | #4C9CEC | 11.22 | 12.47 | the DeepInfra press-kit logo pack (deepinfra.com/media-center → DEEPINFRA_LOGO_COLOR / DEEPINFRA_LOGO_WHITE) |
| `DeepLIcon` | `deepl` | pair | #0F2B46 | #FFFFFF | 13.97 | 12.47 | DeepL's official logo pack on deepl.com/en/press ("Logo Deep Blue" RGB #0F2B46 and the published "Logo White" reversed variant) |
| `DeepSeekIcon` | `deepseek` | pair | #4D6BFE | #6799FE | 4.19 | 4.49 | deepseek.com design tokens (--ds-color-brand under :root / [data-theme=dark]) |
| `DenoIcon` | — | pair | #000000 | #FFFFFF | 20.32 | 12.47 | the "Deno Logo Guidelines 2024" asset pack on deno.com/brand |
| `DigitalOceanIcon` | `digitalocean` | fixed | #0080FF | #0080FF | 3.67 | 3.29 | DigitalOcean's official logo kit (DO_Logo_icon_blue.svg, linked from digitalocean.com/press) |
| `DiscordIcon` | `discord`, `discord_webhook` | mixed | #5865F2 | #5865F2 | 4.46 | 5.71 | https://discord.com/branding |
| `DiscourseIcon` | `discourse` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | discourse.org/brand (discourse-icon.svg / discourse-icon-dark.svg) |
| `DocSpringIcon` | `docspring` | fixed | #3C8EE0 | #3C8EE0 | 3.31 | 12.47 | DocSpring's own logo SVG, docspring.com/assets/logo-text-*.svg |
| `DockerIcon` | — | fixed | #2560FF | #2560FF | 4.84 | 2.49 | Docker's official logo kit (docker.com/company/newsroom/media-resources) |
| `DocusignIcon` | `docusign` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | brand.docusign.com/logo: only the Nexus overlap flips per background, Cobalt #4C00FF and Poppy #FF5252 must not be recoloured |
| `DropboxIcon` | `dropbox` | fixed | #0061FE | #0061FE | 4.91 | 2.46 | brand.dropbox.com/logo and the DIG token dig-color__primary__base |
| `DuckDbIcon` | `duckdb` | pair | #1A1A1A | #FFF100 | 16.84 | 10.59 | duckdb.org/design logo package (DuckDB_icon-lightmode.svg / DuckDB_icon-darkmode.svg) |
| `DucklakeIcon` | — | pair | #1A1A1A | #2EAFFF | 16.84 | 5.16 | duckdb.org |
| `DustIcon` | `dust` | fixed | #FE9C1A | #FE9C1A | 4.04 | 10.64 | dust.tt/home/brand-resources (Dust_LogoSquare.svg from their brand kit) |
| `DynatraceIcon` | `dynatrace` | fixed | #1496FF | #1496FF | 10.01 | 7.83 | Dynatrace brand guidelines (live.standards.site/dynatrace, Dynatrace_mark_color.svg) |
| `EdgeDbIcon` | `edgedb` | fixed | #8FAF24 | #8FAF24 | 2.44 | 4.94 | geldata.com (favicon/apple-touch-icon glyph and its <link rel="mask-icon" color>) |
| `EnodeIcon` | `enode` | pair | #5D770D | #E8E8E1 | 4.94 | 10.13 | enode.com/static/favicon.svg |
| `EventbriteIcon` | `eventbrite` | fixed | #FF5E30 | #FF5E30 | 2.95 | 4.10 | the 2025 Eventbrite press kit logos; the brand publishes no reversed variant |
| `ExaIcon` | `exa` | pair | #0143D9 | #FFFFFF | 7.24 | 12.47 | exa.ai/brand (Exa Brand Assets kit, Logomark Blue/White) |
| `FaunadbIcon` | `faunadb` | pair | #3F00A5 | #604BE9 | 11.58 | 2.19 | Fauna's own VS Code extension icons (fauna/fauna-vscode: icons/fauna.svg for light themes, icons/fauna-light.svg for dark) |
| `FigmaIcon` | `figma` | fixed | #24CB71 | #24CB71 | 4.42 | 5.85 | static.figma.com/app/icon/2/favicon.svg (2025 brand refresh) |
| `FirebaseIcon` | `firebase` | fixed | #FF9100 | #FF9100 | 4.58 | 7.81 | firebase.google.com/brand-guidelines (Logomark_Full Color.svg in firebase-brand-assets.zip) |
| `FlyIcon` | `fly` | pair | #24175B | #FFFFFF | 15.07 | 12.47 | fly.io |
| `FormstackIcon` | `formstack` | fixed | #21B573 | #21B573 | 2.56 | 4.70 | the brand guide at formstack.com/press-kit |
| `FoxentryIcon` | `foxentry` | fixed | #E74600 | #E74600 | 5.09 | 4.14 | foxentry.com/assets/img/logo-foxentry-symbol.svg |
| `FreshdeskIcon` | `freshdesk` | fixed | #20A849 | #20A849 | 3.01 | 12.47 | Freshworks' own product-logo asset (freshdesk-dew.svg, used on freshworks.com/apps) |
| `FrontAppIcon` | `frontapp` | fixed | #A857F1 | #A857F1 | 3.83 | 3.15 | the logo mark front.com ships inline on its own pages; the mark keeps this purple on both light and dark backgrounds |
| `FunkwhaleIcon` | `funkwhale` | mixed | #009FE3 | #009FE3 | 10.69 | 5.71 | www.funkwhale.audio/logos (theme/images/icon.svg) |
| `GSheetsIcon` | `gsheets` | fixed | #009954 | #009954 | 3.57 | 12.47 | Google product logo sheets_2026q3 (gstatic productlogos, used on workspace.google.com/products/sheets) |
| `GcalIcon` | `gcal` | fixed | #BBE2FF | #BBE2FF | 3.40 | 12.47 | Google's own Calendar 2026 product logo, https://www.gstatic.com/images/branding/productlogos/calendar_2026/v2/web/192px.svg (paths verbatim) |
| `GdocsIcon` | `gdocs` | inherits | #718096 | #A9B0BA | 3.88 | 5.71 | Google's own Docs product icon (gstatic.com/images/branding/productlogos/docs_2026/v2/web/192px.svg, served on workspace.google.com/products/docs) |
| `GdriveIcon` | `gdrive` | fixed | #B43333 | #B43333 | 5.87 | 10.05 | https://www.gstatic.com/images/branding/productlogos/drive_2026/v2/web/192px.svg, Google's own product-logo CDN; paths and gradient stops are verbatim |
| `GhostCmsIcon` | `ghostcms` | pair | #15171A | #FFFFFF | 17.38 | 12.47 | docs.ghost.org |
| `GiphyIcon` | `giphy` | fixed | #FFF35C | #FFF35C | 4.76 | 10.83 | GIPHY's own app icon (giphy.com/static/img/icons/apple-touch-icon-180px.png) |
| `GitBookIcon` | `gitbook` | pair | #181C1F | #F2F7F7 | 16.59 | 11.54 | the GitBook-icon-dark / GitBook-icon-light downloads on gitbook.gitbook.io/brand-assets, matching the live gitbook.com favicon |
| `GitIcon` | `git_repository`, `git` | fixed | #F03C2E | #F03C2E | 3.77 | 3.20 | git-scm.com/community/logos (Git-Icon-1788C.svg, logo by Jason Long, CC BY 3.0) |
| `GithubIcon` | `github` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | brand.github.com/foundations/logo |
| `GitlabIcon` | `gitlab` | mixed | #FC6D26 | #FC6D26 | 4.01 | 6.18 | https://design.gitlab.com/brand-design/color (Orange 03p/02p/01p, "colors from our core logo") |
| `GmailIcon` | `gmail` | mixed | #4285F4 | #4285F4 | 5.61 | 7.30 | gstatic.com/images/branding/product/2x/gmail_2020q4_48dp.png |
| `GoogleAiIcon` | `googleai` | fixed | #217BFE | #217BFE | 3.81 | 5.44 | Google's standard Gemini product icon (gstatic.com/images/branding/productlogos/gemini/v1/192px.svg) |
| `GoogleCalendarIcon` | — | fixed | #BBE2FF | #BBE2FF | 3.40 | 12.47 | the Google Calendar 2026 product icon, taken verbatim from https://www.gstatic.com/images/branding/productlogos/calendar_2026/v2/web/192px.svg |
| `GoogleCloudIcon` | `gcloud`, `gcp_service_account` | fixed | #EA4335 | #EA4335 | 3.80 | 7.30 | Google's own product logo asset https://www.gstatic.com/images/branding/product/2x/google_cloud_64dp.png |
| `GoogleDriveIcon` | — | fixed | #B43333 | #B43333 | 5.87 | 10.05 | https://www.gstatic.com/images/branding/productlogos/drive_2026/v2/web/192px.svg (Drive 2026 mark, copied verbatim) |
| `GoogleFormsIcon` | `gforms` | fixed | #969DFF | #969DFF | 5.99 | 12.47 | Google's own Forms product icon at www.gstatic.com/images/branding/productlogos/forms_2026/v2/web/192px.svg |
| `GoogleIcon` | `google`, `gworkspace` | mixed | #4285F4 | #4285F4 | 3.88 | 7.30 | the G mark Google serves in accounts.google.com/gsi/client |
| `GorgiasIcon` | `gorgias` | pair | #000000 | #FFF9F4 | 20.32 | 11.94 | gorgias.com/about-us/style, which ships the symbol as a "Dark"/"Light" pair |
| `GraphqlIcon` | `graphql` | pair | #E10098 | #FFFFFF | 4.37 | 12.47 | graphql.org |
| `GreipIcon` | `greip` | pair | #141C27 | #FFFFFF | 16.59 | 12.47 | docs.greip.io |
| `GristIcon` | `grist` | fixed | #16B378 | #16B378 | 2.62 | 8.25 | getgrist.com/trademark/assets/ |
| `GroqIcon` | `groqai`, `groq` | fixed | #F43E01 | #F43E01 | 3.67 | 12.47 | https://groq.com/favicon.svg |
| `HackernewsIcon` | `hackernews` | fixed | #FF6600 | #FF6600 | 2.84 | 12.47 | news.ycombinator.com/y18.svg |
| `HoldedIcon` | `holded` | fixed | #FD454D | #FD454D | 3.31 | 3.64 | cdn.holded.com/assets/img/brand/holded-logo.svg |
| `HoneybadgerIcon` | `honeybadger` | fixed | #EA5937 | #EA5937 | 3.40 | 3.55 | honeybadger.io/favicon.svg |
| `HtmlIcon` | — | fixed | #E44D26 | #E44D26 | 3.77 | 12.47 | the W3C HTML5 logo, w3.org/html/logo (downloads/HTML5_Logo.svg) |
| `HubspotIcon` | `hubspot` | pair | #FF2F00 | #FFFFFF | 3.59 | 12.47 | hubspot.com |
| `IfsIcon` | `ifs_cloud_oidc` | fixed | #72C9F8 | #72C9F8 | 6.03 | 6.79 | the IFS symbol on ifs.com |
| `IftttIcon` | `ifttt` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | ifttt.com |
| `InkeepIcon` | `inkeep` | fixed | #D5E5FF | #D5E5FF | 2.45 | 9.79 | Inkeep's brand page "Icon Core" (https://inkeep.com/brand) |
| `IntercomIcon` | `intercom` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | intercom.com |
| `IpinfoIcon` | `ipinfo` | pair | #3091CF | #FFFFFF | 3.34 | 12.47 | ipinfo.io logo-positive.svg and logo-negative.svg |
| `JavaIcon` | — | fixed | #007396 | #007396 | 5.22 | 4.93 | Oracle's Java Branding and Licensing Guidelines v21 (oracle.com/a/ocom/docs/java-licensing-logo-guidelines-1908204.pdf) |
| `JavaScriptIcon` | — | fixed | #F7DF1E | #F7DF1E | 20.32 | 9.22 | js.svg in github.com/voodootikigod/logo.js, the origin of the JavaScript logo |
| `JiraIcon` | `jira` | fixed | #1868DB | #1868DB | 5.03 | 12.47 | Atlassian's official Jira logo pack (atlassian.design/foundations/logos) |
| `JoomlaIcon` | `joomla` | fixed | #7AC143 | #7AC143 | 3.58 | 6.23 | the official logo at cdn.joomla.org/images/joomla-colours-logo.svg |
| `JotformIcon` | `jotform` | pair | #0A1551 | #FFFFFF | 16.40 | 12.47 | jotform.com footer logomark (#jotform-logomark-fourth is filled with --jf-logo-img: #0A1551 light, #fff dark) |
| `JsonIcon` | — | fixed | #F9A825 | #F9A825 | 1.91 | 6.33 | Material Design Yellow 800 (api.flutter.dev Colors.yellow[800]); glyph is Google's Material Symbols "data_object" |
| `JumpCloudIcon` | `jumpcloud` | pair | #002B49 | #F7F7FB | 14.09 | 11.67 | jumpcloud.com/press (Ocean Blue / White Smoke); White Smoke is the brand's own reversed logo for dark backgrounds |
| `KafkaIcon` | `kafka` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | apache/kafka |
| `KanidmIcon` | `kanidm` | fixed | #B1DEF4 | #B1DEF4 | 20.32 | 12.47 | artwork/logo-square.svg in github.com/kanidm/kanidm (full palette: #FF6600 #803300 #D45500 #2A3455 #B1B3B8 #CCCCCC) |
| `KeycloakIcon` | `keycloak` | fixed | #00B8E3 | #00B8E3 | 8.18 | 10.65 | keycloak.org's own mark, https://www.keycloak.org/resources/images/icon.svg (cyan #00B8E3/#33C6E9/#008AAA over greys #4D4D4D#EDEDED, single theme) |
| `KlaviyoIcon` | `klaviyo` | pair | #1D1E20 | #FFFFFF | 16.14 | 12.47 | klaviyo.com --color-core-charcoal; the flag mark is the standalone logomark the site header collapses to, and the shape of klaviyo.com/icons/icon-512x512.png |
| `KoboToolboxIcon` | `kobotoolbox` | fixed | #2095F3 | #2095F3 | 3.05 | 3.95 | the kobotoolbox.org header logo and $kobo-blue in kobotoolbox/kpi jsapp/scss/colors.scss |
| `KustomerIcon` | `kustomer` | fixed | #FBEC2A | #FBEC2A | 14.08 | 12.47 | kustomer.com/images/kustomer/Kusty.svg |
| `LangfuseIcon` | `langfuse` | fixed | #FF5D5F | #FF5D5F | 2.91 | 4.47 | langfuse.com/brand "Icon - Color (SVG)", used unmodified |
| `LessIcon` | — | pair | #274F82 | #FFFFFF | 8.04 | 12.47 | github.com/less/logo (MIT) |
| `LineIcon` | `line` | fixed | #06C755 | #06C755 | 2.18 | 5.53 | LINE's official brand icon asset (line.me/en/logo) |
| `LinearIcon` | `linear` | pair | #222326 | #F4F5F8 | 15.20 | 11.44 | linear.app/brand |
| `LinkdingIcon` | — | pair | #5856E0 | #ADABF7 | 5.32 | 5.91 | sissbruecker/linkding |
| `LinkedinIcon` | `linkedin` | mixed | #0A66C2 | #0A66C2 | 5.50 | 12.47 | the official inbug SVGs embedded in brand.linkedin.com/in-logo |
| `LinodeIcon` | `linode` | fixed | #004B16 | #004B16 | 10.07 | 4.54 | Linode's own packages/manager/src/assets/logo/logo.svg in linode/manager @3e53c92, the last revision before the Akamai rebrand dropped it |
| `LumaAiIcon` | `lumaai` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | lumalabs.ai (favicon-black.ico on light, favicon-white.ico on dark) |
| `MSSqlServerIcon` | — | fixed | #0094F0 | #0094F0 | 10.54 | 12.47 | learn.microsoft.com/en-us/azure/architecture/icons — Microsoft's anchor blue, a stop in its own SQL Server SVG and throughout the set's Fluent gradients |
| `MSTeamsIcon` | — | fixed | #A98AFF | #A98AFF | 12.96 | 12.47 | Microsoft's Teams-Icon-FY26 asset (cdn-dynmedia-1.microsoft.com, served on microsoft.com/microsoft-teams); every gradient stop here is verbatim from it |
| `MagentoIcon` | `magento` | fixed | #F26322 | #F26322 | 3.09 | 3.91 | Magento's own logo asset, magento2 lib/web/images/logo.svg |
| `MailchimpIcon` | `mailchimp` | fixed | #241C15 | #241C15 | 16.23 | 10.74 | mailchimp.com/about/brand-assets |
| `MailerLiteIcon` | `mailerlite` | fixed | #09C269 | #09C269 | 2.27 | 5.31 | mailerlite.com/brand-assets |
| `MailgunIcon` | `mailgun` | fixed | #F04126 | #F04126 | 3.70 | 12.47 | mailgun.com's own logo-mailgun-icon.svg |
| `MandrillIcon` | `mandrill` | pair | #241C15 | #FFFFFF | 16.23 | 12.47 | mailchimp.com/about/brand-assets and Mandrill's own mandrillapp.com/img/navigation/freddie.svg |
| `MapboxIcon` | `mapbox` | pair | #0E1012 | #FFFFFF | 18.45 | 12.47 | mapbox.com |
| `MarkdownIcon` | — | pair | #000000 | #FFFFFF | 20.32 | 12.47 | dcurtis/markdown-mark (public domain) |
| `MastodonIcon` | `mastodon` | mixed | #6364FF | #6364FF | 7.07 | 12.47 | https://joinmastodon.org/branding |
| `MatrixIcon` | `matrix` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | matrix.org/branding |
| `MatteroomIcon` | `matteroom` | fixed | #134A81 | #134A81 | 8.74 | 12.47 | the MATTEROOM logomark vector at login.matteroom.com/images/login_logo.svg; square tile proportions taken from their own app icon at matteroom.com/favicon.ico |
| `MauticIcon` | `mautic` | pair | #4E5E9E | #FFFFFF | 5.94 | 12.47 | mautic.org/about/brand-logos-graphics (Mautic_Logo_LB.svg / Mautic_Logo_DB.svg); the "M" stays Sunglow #FDB933 in both, as the trademark policy requires the mark in its exact published form without alteration in colour |
| `McpIcon` | `mcp` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | modelcontextprotocol/modelcontextprotocol |
| `MediumIcon` | `medium` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | medium.design |
| `MeteosourceIcon` | `meteosource` | fixed | #FAD961 | #FAD961 | 2.87 | 9.01 | the logo in the meteosource.com site header (no brand page published) |
| `MezmoIcon` | `mezmo` | pair | #0A090C | #E6E6E5 | 19.22 | 9.99 | mezmo.com nav mark and docs.mezmo.com logo/light.png + logo/dark.png |
| `MicrosoftIcon` | `microsoft` | mixed | #F25022 | #F25022 | 3.88 | 7.24 | the official logo asset linked from Microsoft's logo third-party usage guidance |
| `MiroIcon` | `miro` | fixed | #FFDD33 | #FFDD33 | 16.46 | 9.29 | the Miro logo on miro.com |
| `MistralIcon` | `mistral` | inherits | #718096 | #A9B0BA | 3.88 | 5.71 | mistral.ai/favicon.svg (mid-band of the #FFAF01 -> #C4001D ramp); drawn here in currentColor, the monochrome variant mistral.ai/brand ships |
| `MixpanelIcon` | `mixpanel` | pair | #7856FF | #FFFFFF | 4.44 | 12.47 | brand.mixpanel.com/logo and /color (Purple 100); mixpanel.com ships the same pair as its light/dark favicons |
| `MollieIcon` | `mollie` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | Mollie's app icon (my.mollie.com/assets/images/favicons/apple-touch-icon-180x180.png): a full-bleed disc with the lowercase m knocked out |
| `MondayIcon` | `monday` | fixed | #FB275D | #FB275D | 3.66 | 8.25 | monday.com's official logo pack (brand-monday.com/logo) |
| `MongodbIcon` | `mongodb` | pair | #00684A | #00ED64 | 6.60 | 7.90 | MongoDB brand resources and their LeafyGreen palette |
| `MotimateIcon` | `motimate` | fixed | #2DC89C | #2DC89C | 2.06 | 5.85 | motimateapp.com theme assets |
| `MqttIcon` | `mqtt` | pair | #660066 | #FFFFFF | 11.57 | 12.47 | mqtt/mqttorg-graphics |
| `Mysql` | `mysql` | inherits | #718096 | #A9B0BA | 3.88 | 5.71 | — |
| `NatsIcon` | `nats` | pair | #375C93 | #27AAE1 | 6.52 | 4.71 | cncf/artwork |
| `NeonDbIcon` | `neondb` | pair | #37C38F | #34D59A | 2.17 | 6.61 | neon.com/brand (neon-logomark-light-color.svg / neon-logomark-dark-color.svg) |
| `NetBoxIcon` | `netbox` | pair | #001423 | #FFFFFF | 18.07 | 12.47 | theme, so the second path carries its own fill- utilities |
| `NetlifyIcon` | `netlify` | pair | #05BDBA | #32E6E2 | 10.06 | 12.47 | netlify.com/brand (netlify-logo-monogram.zip, full-colour lightmode/darkmode) |
| `NetsuiteIcon` | `netsuite` | fixed | #BACCDB | #BACCDB | 7.71 | 7.57 | — |
| `NewsApiIcon` | `newsapi` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | newsapi.org |
| `NextcloudIcon` | `ocs`, `nextcloud` | pair | #0082C9 | #FFFFFF | 4.03 | 12.47 | nextcloud.com |
| `NocoDbIcon` | `nocodb` | fixed | #4351E8 | #4351E8 | 11.12 | 3.30 | nocodb.com's own Logo.svg / favicon |
| `NotionIcon` | `notion` | fixed | #FFFFFF | #FFFFFF | 20.32 | 12.47 | Notion's own app icon (notion.com/front-static/logo-ios.png) |
| `NuIcon` | — | fixed | #4D9B05 | #4D9B05 | 3.38 | 3.57 | nushell/vscode-nushell-lang assets/nu.svg |
| `OdkIcon` | `odk` | fixed | #3E77B4 | #3E77B4 | 6.37 | 2.67 | ODK brand assets (getodk.org/legal/brand/) |
| `OktaIcon` | `okta` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | okta.com |
| `OneSignalIcon` | `onesignal` | pair | #051B2C | #FFFFFF | 16.94 | 12.47 | OneSignal's official media kit (OneSignal-Logomark.svg / OneSignal-Logomark-White.svg), matching the prefers-color-scheme pair in their own onesignal.com/favicon.svg |
| `OpenRouterIcon` | `openrouter` | pair | #7624F4 | #C8FF00 | 6.10 | 10.55 | openrouter.ai/brand/v2/openrouter-glyph-{light,dark}.svg |
| `OpenWeatherIcon` | `openweather` | pair | #EA6D4A | — | 2.99 | — | openweather.co.uk/brand_guidelines |
| `OpenaiIcon` | `openai` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | openai.com/brand (Blossom_Light.svg / Blossom_Dark.svg) |
| `OracleDBIcon` | `oracledb` | fixed | #C74634 | #C74634 | 4.67 | 2.59 | Oracle's own logo SVG at https://www.oracle.com/a/ocom/img/oracle-logo.svg |
| `OutreachIcon` | `outreach` | pair | #5951FF | #FFFFFF | 5.02 | 12.47 | outreach.ai |
| `PHPIcon` | — | fixed | #AEB2D5 | #AEB2D5 | 20.32 | 12.47 | php.net/images/logos/new-php-logo.svg (php.net/download-logos.php) |
| `PagerDutyIcon` | `pagerduty` | pair | #048A24 | #FFFFFF | 4.35 | 12.47 | pagerduty.com/brand "P icon" pack (P-GreenRGB.svg / P-WhiteRGB.svg) |
| `PandaDocIcon` | `pandadoc` | fixed | #248567 | #248567 | 4.39 | 12.47 | the PandaDoc logo shipped on pandadoc.com (header logo SVG and favicon); white monogram on the green tile in both themes |
| `PaychexIcon` | `paychex` | fixed | #004B8D | #004B8D | 8.50 | **1.42** | paychex.com's own logo SVG (themes/custom/paychex2/images/svg/logo-paychex.svg, .st0) |
| `PaylocityIcon` | `paylocity` | fixed | #ED2024 | #ED2024 | 4.20 | 12.47 | paylocity.com design-system CSS (.styleBGBrandGradient) |
| `PaypalIcon` | `paypal` | fixed | #002991 | #002991 | 11.77 | 6.93 | PayPal's own paypal-mark-color_new.svg (site header logo on paypal.com) |
| `PersonaIcon` | `persona` | fixed | #7379FD | #7379FD | 3.45 | 3.49 | https://withpersona.com/favicon.svg (Persona, identity verification) |
| `PersonioIcon` | `personio` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | personio.design/brand/how-we-look/logo |
| `PhraseIcon` | `phrase` | pair | #181818 | #FFFFFF | 17.18 | 12.47 | Logo_primary.svg and Logo_black_background.svg on phrase.com/brand |
| `PineconeIcon` | `pinecone` | pair | #201D1E | #FFFFFF | 16.18 | 12.47 | pinecone.io/newsroom/media-kit |
| `PinterestIcon` | `pinterest` | fixed | #E60023 | #E60023 | 4.63 | 2.61 | Pinterest Gestalt tokens (color.icon.brand.primary = red.pushpin.450, identical in sema-color-light and sema-color-dark) |
| `PipedriveIcon` | `pipedrive` | fixed | #017737 | #017737 | 5.50 | 12.47 | pipedrive.com logo token --pd-puco-global-color-green-500 |
| `PlanetScaleIcon` | `planetscale` | pair | #1A1A1A | #FAFAFA | 16.84 | 11.95 | planetscale.com |
| `PocketIdIcon` | `pocketid` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | pocket-id.org header logo (fill isDark ? #ffffff : #000000) and pocket-id/pocket-id frontend/src/lib/components/logo.svelte |
| `PostgresIcon` | `postgresql` | mixed | #336791 | #336791 | 20.32 | 12.47 | the official 3-colour Slonik SVG on wiki.postgresql.org/wiki/Logo |
| `PostmarkIcon` | `postmark` | fixed | #FFDE00 | #FFDE00 | 20.32 | 9.33 | postmarkapp.com/images/logo-stamp-simple.svg |
| `PowershellIcon` | — | fixed | #00FF18 | #00FF18 | 20.32 | 12.47 | github.com/PowerShell/PowerShell/blob/master/assets/ps_black_64.svg |
| `PusherIcon` | `pusher` | pair | #300D4F | #FFFFFF | 15.68 | 12.47 | pusher.com media kit (Pusher logo primary.png / Pusher logo secondary.png) |
| `PushoverIcon` | `pushover` | fixed | #249DF1 | #249DF1 | 2.83 | 12.47 | support.pushover.net/i63-pushover-logos-and-usage |
| `QoveryIcon` | `qovery` | fixed | #642DFF | #642DFF | 6.05 | 2.00 | qovery.com/logos/qovery-logo-black.svg |
| `QuickbooksIcon` | `quickbooks` | fixed | #2CA01C | #2CA01C | 3.30 | 3.65 | the QuickBooks logo SVG on intuit.com's press room |
| `RIcon` | — | fixed | #276DC3 | #276DC3 | 6.46 | 7.89 | r-project.org/logo (gradient stops copied from the authoritative Rlogo.svg) |
| `RaindropIcon` | `raindrop` | fixed | #1988E0 | #1988E0 | 5.33 | 12.47 | app.raindrop.io/assets/icon_raw.svg and raindrop.io icon_128.png |
| `ReactIcon` | — | pair | #087EA4 | #58C4DC | 4.48 | 6.14 | react.dev brand menu (images/brand/logo_light.svg, logo_dark.svg) |
| `ReadmeIcon` | `readme` | pair | #213AFF | #FFFFFF | 6.53 | 12.47 | readme.com's own prefers-color-scheme favicon pair (favicon-213aff.ico / favicon-ffffff.ico) |
| `ReadwiseIcon` | `readwise` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | readwise.io's logo-standalone-dark.svg (light) and logo-standalone-white.svg (dark) |
| `RecraftIcon` | `recraft` | fixed | #000000 | #000000 | 20.32 | 12.47 | Recraft's press-kit "Icon White" mark (https://www.recraft.ai/press-releases) |
| `RedditIcon` | `reddit` | fixed | #FF6600 | #FF6600 | 20.32 | 12.47 | redditinc.com/brand ("a stylized Snoo head contained within an OrangeRed (#FF4500) conversation bubble") |
| `RenderIcon` | `render` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | render.com |
| `ReplicateIcon` | `replicate` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | replicate.com header logo (glyph is currentColor; site CSS sets #000, and #FFF under .dark) |
| `ResendIcon` | `resend` | pair | #000000 | #FDFDFD | 20.32 | 12.26 | cdn.resend.com/brand/resend-icon-black.svg and resend-icon-white.svg |
| `RingCentralIcon` | `ringcentral` | pair | #FF7A00 | #FFFFFF | 2.53 | 12.47 | assets.ringcentral.com/us/brand-library/logos/ringcentral-logo.zip (RingCentral logo fullcolor.svg / RingCentral logo white.svg) |
| `RocketChatIcon` | `rocketchat` | fixed | #F5455C | #F5455C | 3.45 | 3.50 | Rocket.Chat brand colours (docs.rocket.chat/v1/docs/colors), the primary red of their logo |
| `RssIcon` | `rss` | fixed | #FFA500 | #FFA500 | 1.91 | 12.47 | Mozilla's feed icon guidelines (mozilla.org/en-US/foundation/feed-icon-guidelines/), which fix no exact hex |
| `RubyIcon` | — | fixed | #FB7655 | #FB7655 | 10.63 | 12.47 | the official logo kit at ruby-lang.org/en/about/logo |
| `RunPodIcon` | `runpod` | pair | #5D29F0 | #FFFFFF | 6.68 | 12.47 | runpod.io/brandkit |
| `RustIcon` | — | pair | #000000 | #FFFFFF | 20.32 | 12.47 | rust-lang/rust-artwork |
| `S3Icon` | `s3` | fixed | #7AA116 | #7AA116 | 2.93 | 12.47 | AWS Architecture Icons (Icon-package_07312026, Arch_Storage/64/Arch_Amazon-Simple-Storage-Service_64.svg) |
| `SageIcon` | `sage_intacct` | pair | #000000 | #00D639 | 20.32 | 6.34 | @sage/design-tokens --logo-sage-bg-default |
| `SalesflareIcon` | `salesflare` | fixed | #0053FF | #0053FF | 5.52 | 2.19 | salesflare.com's own `--color--major-blue` design token |
| `SalesforceIcon` | `salesforce` | fixed | #00B3FF | #00B3FF | 2.29 | 5.28 | brand.salesforce.com/brand/color |
| `SassIcon` | — | fixed | #CC6699 | #CC6699 | 3.43 | 12.47 | sass-lang.com's own style guide token --sl-color--hopbush (assets/dist/css/sass.css) |
| `SegmentIcon` | `segment` | fixed | #52BD94 | #52BD94 | 2.24 | 5.38 | Segment's own app favicon (app.segment.com) and Evergreen green500 #52BD95 |
| `SendflakeIcon` | `snowflake` | pair | #29B5E8 | — | 2.29 | — | snowflake.com/brand-guidelines |
| `SendgridIcon` | `sendgrid` | fixed | #00B3E3 | #00B3E3 | 3.81 | 8.57 | styleguide.sendgrid.com/colors.html |
| `SensorTowerIcon` | `sensortower` | fixed | #00CFB8 | #00CFB8 | 1.91 | 12.47 | sensortower.com/favicon.svg, copied verbatim |
| `SentryIcon` | `sentry` | pair | #181225 | #FFFFFF | 17.64 | 12.47 | sentry.io/branding logo generator (Dark/Light themes, "Invert in dark mode") |
| `ServiceNowIcon` | `servicenow` | fixed | #62D84E | #62D84E | 1.77 | 6.80 | servicenow.com/company/servicenow-logo.html (servicenow-logo-icon.svg) |
| `ShopifyIcon` | `shopify` | fixed | #95BF47 | #95BF47 | 3.75 | 12.47 | shopify.com/brand-assets (shopify-logo-shopping-bag-full-color.svg: #95BF47/#5E8E3E/#fff) |
| `ShortcutIcon` | `shortcut` | pair | #494BCB | #797ADE | 6.45 | 3.36 | shortcut.com/branding (mark-default.svg; the reversed lockup uses #797ADE on dark) |
| `ShutterstockIcon` | `shutterstock` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | brand.shutterstock.com |
| `SigNozIcon` | `signoz` | fixed | #FF5E19 | #FF5E19 | 3.63 | 12.47 | signoz.io/img/SigNozLogo-orange.svg |
| `Slack` | `slack` | mixed | #E01E5A | #E01E5A | 4.51 | 6.52 | slack.com's own nav logo (a.slack-edge.com/38f0e7c/marketing/img/nav/logo.svg, linked from slack.com/media-kit) |
| `SmartsheetIcon` | `smartsheet` | pair | #031C59 | #FFFFFF | 15.46 | 12.47 | brandguides.brandfolder.com/smartsheet-visual-guide/basics |
| `SnowflakeIcon` | — | pair | #29B5E8 | — | 2.29 | — | snowflake.com/brand-guidelines |
| `SpeechifyIcon` | `speechify` | pair | #2F43FA | #FFFFFF | 6.15 | 12.47 | the Speechify brand kit (speechify.com/brand-kit, Logomark_blue.svg and Logomark_white.svg) |
| `SplitwiseIcon` | `splitwise` | pair | #1CC29F | — | 2.19 | — | splitwise.com/press (sw.svg / sw-wide.svg / bg-primary.svg) |
| `SpotifyIcon` | `spotify` | pair | #1ED760 | #FFFFFF | 1.86 | 12.47 | developer.spotify.com/documentation/design (2024 Primary Logo icon pack) |
| `SquareIcon` | `square` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | Square_Logo_2025 in squareup.com/us/en/press/logo |
| `StraleIcon` | `strale` | pair | #0D0D0E | #F2F2F3 | 18.80 | 11.15 | strale.dev favicon.svg and the site's own --foreground token |
| `StravaIcon` | `strava` | fixed | #FC5200 | #FC5200 | 3.20 | 3.77 | developers.strava.com/guidelines (Strava API logo pack, orange SVGs) |
| `StripeIcon` | `stripe` | fixed | #533AFD | #533AFD | 5.99 | 12.47 | Stripe's own favicon.svg and Stripe_logo_kit.zip (stripe.com/newsroom/brand-assets) |
| `SupabaseIcon` | `supabase` | fixed | #3ECF8E | #3ECF8E | 3.75 | 6.25 | supabase.com/brand-assets |
| `SurrealdbIcon` | `surrealdb` | mixed | #D255FE | #D255FE | 7.33 | 5.71 | surrealdb.com/brand |
| `SvelteIcon` | — | fixed | #FF3E00 | #FF3E00 | 3.42 | 12.47 | sveltejs/branding (svelte-logo.svg, white cutout #fff) |
| `TallyIcon` | `tally` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | the "Tally Icon - Black" / "Tally Icon - White" files in the icon pack on tally.so/help/press-kit, matching the live tally.so/favicon.svg |
| `TaskadeIcon` | `taskade` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | taskade.com/press (Mascot Mark light = agent_taskade.svg, Genesis Icon dark = taskade-icon-dark.svg) |
| `TelegramIcon` | `telegram` | fixed | #2AABEE | #2AABEE | 2.92 | 12.47 | Telegram's press-kit Logo.svg (telegram.org/press) |
| `TelnyxIcon` | `telnyx` | pair | #000000 | #00E3AA | 20.32 | 12.47 | telnyx.com |
| `TerraIcon` | `terra` | fixed | #008AFF | #008AFF | 20.32 | 10.43 | tryterra.co/providers/terra_icon.svg, the only vector square mark Terra ships (the site logo is a "TERRA API" wordmark, the favicon a raster .ico) |
| `TheirStackIcon` | `their_stack` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | theirstack.com/en/docs/brand, which lists both as core brand colours |
| `ThreadsIcon` | `threads` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | Meta's Threads Brand Resource Center logo pack (meta.com/brand/resources/threads) |
| `TodoistIcon` | `todoist` | fixed | #E44232 | #E44232 | 3.97 | 3.04 | Todoist Brand Guidelines (doist.com/brand-assets/todoist-logo.zip), "Red — the primary brand color for Todoist" |
| `TogetherAiIcon` | `togetherai` | fixed | #EF2CC1 | #EF2CC1 | 3.50 | 6.46 | together.ai's brand page (https://www.together.ai/brand) |
| `TogglIcon` | `toggl` | pair | #2C1138 | #E57CD8 | 16.33 | 4.87 | Toggl Track media toolkit (toggl.com/track/media-toolkit, icon-dark-purple.svg / icon-pink.svg) |
| `TomorrowIoIcon` | `tomorrow` | fixed | #004CF8 | #004CF8 | 6.00 | 12.47 | tomorrow.io's own design tokens (--color-logo-blue in site-frame.min.css, matching the header lockup SVG and logo-490.png) |
| `TrelloIcon` | `trello` | fixed | #1558BC | #1558BC | 6.44 | 12.47 | Atlassian Design logo library (atlassian.design/foundations/logos → trello_app.zip, Trello_icon.svg) |
| `TripadvisorIcon` | `tripadvisor` | fixed | #002B11 | #002B11 | 15.01 | **1.24** | 2025 Tripadvisor Brand Guidelines for Partners, tripadvisor.mediaroom.com |
| `TursoIcon` | `turso` | pair | #183134 | #FFFFFF | 13.30 | 12.47 | turso.tech/brand (Dark Teal and white logomark variants) |
| `TwilioIcon` | `twilio` | fixed | #F22F46 | #F22F46 | 3.86 | 3.13 | twilio.com (mask-icon color, favicon and apple-touch-icon artwork) |
| `TwitchIcon` | `twitch` | fixed | #9146FF | #9146FF | 4.49 | 2.69 | brand.twitch.com |
| `TwitterIcon` | `twitter` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | about.x.com |
| `TypeformIcon` | `typeform` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | typeform.com/brand |
| `UltravoxIcon` | `ultravox` | fixed | #BB3B57 | #BB3B57 | 6.67 | 7.65 | the ultravox.ai favicon (framerusercontent.com/images/hzAEdihxJ11mv3l4trNh2WprE.svg) |
| `VectaraIcon` | `vectara` | fixed | #7E00FF | #7E00FF | 7.00 | 9.80 | — |
| `VercelIcon` | `vercel` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | vercel.com |
| `VismaIcon` | `visma` | pair | #131313 | #FFFFFF | 17.98 | 12.47 | design.visma.com/logo (VA symbol from the official Visma Logopack) |
| `VueIcon` | — | fixed | #42B883 | #42B883 | 8.97 | 5.00 | vuejs/art logo.svg |
| `WebflowIcon` | `webflow` | fixed | #146EF5 | #146EF5 | 4.44 | 2.72 | brand.webflow.com/brand-assets |
| `WhatsappBusinessIcon` | `whatsapp_business` | fixed | #25D366 | #25D366 | 1.92 | 6.29 | WhatsApp's Digital_Glyph_Green_RGB_2026.svg, shipped by whatsapp.com/business (→ whatsappbusiness.com) |
| `WizIcon` | `wiz` | pair | #0254EC | #FFFFFF | 5.84 | 12.47 | wiz.io/press media kit logo pack (WizLogo_Blue_Vector.svg / WizLogo_White_Vector.svg) |
| `WooCommerceIcon` | `woocommerce` | pair | #873EFF | #FFFFFF | 4.88 | 12.47 | the Woo logo pack at woocommerce.com/brand-and-logo-guidelines (Woo_logo_color.svg and Woo_logo_white.svg) |
| `WordpressIcon` | `wordpress` | pair | #32373C | #FFFFFF | 11.63 | 12.47 | wordpress.org/about/logos/ |
| `XataIcon` | `xata` | fixed | #8468F6 | #8468F6 | 3.84 | 3.14 | xata.io/brand (logo-symbol.svg) |
| `XeroIcon` | `xero` | fixed | #13B5EA | #13B5EA | 2.30 | 12.47 | xero.com favicon.svg and the site header logo (Xero__LogoPath fill) |
| `YamlIcon` | — | mixed | #CB171E | #CB171E | 5.52 | 5.71 | yaml.org's own assets/favicon.svg and assets/logo.png; the Y, M and L carry no fill in YAML's SVG, so they take the surrounding text colour |
| `YelpIcon` | `yelp` | fixed | #FF1A1A | #FF1A1A | 3.75 | 3.22 | yelp.com/brand (burst_red.svg and the official logo kit) |
| `YnabIcon` | `ynab` | pair | #3B5EDA | #FEF9E6 | 5.35 | 11.82 | ynab.com press kit tree logo (Tree Logo Blurple.svg / Tree Logo Buttermilk.svg — the buttermilk reverse is what ynab.com itself uses on its dark footer) |
| `YoutubeIcon` | `youtube` | fixed | #FF0033 | #FF0033 | 3.83 | 12.47 | brand.youtube/color (YouTube Red, updated from #FF0000) |
| `ZammadIcon` | `zammad` | fixed | #CD2015 | #CD2015 | 7.62 | 9.73 | zammad.com favicon-32x32.svg |
| `ZendeskIcon` | `zendesk` | pair | #11110D | #FFFFFF | 18.31 | 12.47 | zendesk.com |
| `ZeroTierIcon` | `zerotier` | fixed | #FFB25B | #FFB25B | 16.58 | 6.99 | zerotier.com's own icon.svg and logo lockups |
| `ZitadelIcon` | `zitadel` | pair | #232323 | #FFFFFF | 15.21 | 12.47 | zitadel/zitadel console assets zitadel-logo-solo-dark.svg / zitadel-logo-solo-light.svg |
| `ZixflowIcon` | `zixflow` | pair | #141414 | #FFFFFF | 17.83 | 12.47 | docs.zixflow.com logo pack (logo/light.svg / logo/dark.svg) |
| `ZohoIcon` | `zoho` | pair | #000000 | #FFFFFF | 20.32 | 12.47 | zoho.com/branding (zoho-logo-web.svg / zoho-logo-white.svg) |
| `ZoomIcon` | `zoom` | pair | #0B5CFF | #FFFFFF | 5.09 | 12.47 | brand.zoom.com |
| `ZuploIcon` | `zuplo` | fixed | #FF00BD | #FF00BD | 3.39 | 3.56 | https://zuplo.com/brand |
## Rules the brand imposes
Constraints that would otherwise be broken by a well-meaning change.
- **AblyIcon** — "Don't use other colours or gradients for the symbol."
- **AcceloIcon** — The mark keeps these three fills in both themes; only the wordmark (not drawn here) swaps #10202D for white.
- **AmqpIcon** — No brand colour: AMQP is an OASIS protocol, not a vendor, and amqp.org publishes no palette (https://www.amqp.org/legal.html). Generic glyph, deliberately monochrome — keep it on currentColor.
- **AnsibleIcon** — The mark is a solid disc knocked out with a white "A", so the pair is applied by inverting rather than currentColor: recolouring the disc alone would leave white on light grey.
- **ApifyIcon** — White/black variants are reserved for monochromatic contexts, so the tricolour mark stays in both themes.
- **AppwriteIcon** — Brand asks that the logo not be altered, so no per-theme variant.
- **ArcGisIcon** — Esri publishes no reversed variant for this badge and forbids altering its logos.
- **AsanaIcon** — Asana's guidelines forbid recolouring: the symbol always appears in coral, on light and dark backgrounds alike.
- **AssemblyAiIcon** — Two colours per theme, so the second stroke carries its own fill- utilities: #777673 on light, #FFFFFF on dark.
- **AttioIcon** — The mark is a filled compound path; stroking it instead thickens it and leaks the default black fill.
- **Auth0Icon** — Okta's content terms forbid altering the mark, so ship only these published variants.
- **AutheliaIcon** — authelia.com/reference/guides/branding permits format/layout changes only — do not alter the design.
- **AuthentikIcon** — The white variant is the brand's own asset for dark backgrounds; proportions and colour must not be altered otherwise.
- **AwsEcrIcon** — AWS ships one flat fill for both themes; the gradient tile was retired in the 2023 accessibility refresh.
- **AwsIcon** — aws.amazon.com/trademark-guidelines forbids altering the logo's colour, so only these two published variants may be used.
- **BaserowIcon** — The mark keeps these three colours on light and dark; only the wordmark reverses to white.
- **BeamerIcon** — The isotype is monochrome in all first-party artwork.
- **BigQueryIcon** — Google publishes no reversed variant, so the same mark is used on both themes.
- **BitbucketIcon** — Atlassian ships brand/neutral/inverse only: "don't use unapproved color combinations".
- **BitlyIcon** — Bitly's reversed logomark is white over orange, not over neutral dark, so the orange mark is used on both.
- **BloggerIcon** — Google publishes no reversed variant.
- **BlueskyIcon** — The downloadable media-kit butterfly still ships the older #006AFF, but the palette is the normative source: "use only the official color values above. Do not substitute, tint, or approximate." White is the approved monochrome variant for dark backgrounds.
- **BoxIcon** — box.com/legal/trademark forbids any other recolouring of the mark.
- **BrevoIcon** — Brevo publishes no per-theme variant of the app mark; the reversed "Mint" #F9FFF6 asset is the wordmark only.
- **BrowserlessIcon** — Its own prefers-color-scheme block sets black on light, white on dark.
- **BubbleIcon** — Bubble's brand terms forbid re-colouring the mark beyond its published dark/light pair.
- **BuildkiteIcon** — Buildkite ships a single mark "for any context", so there is no per-theme variant, and asks that it not be altered.
- **ButtondownIcon** — Brand forbids recolouring the logo, so no per-theme variant.
- **CSharpIcon** — Its README forbids altering the mark, so the same full-colour icon is used on light and dark.
- **CalcomIcon** — Cal.com's design system states it is deliberately a grayscale brand and publishes exactly two logo variants.
- **CalendlyIcon** — Guidelines: "Only show our logo and lockups in blue or white."
- **CertopusIcon** — Verbatim copy of the brand's own circle mark: #2C353D and the white disc are its other fixed tones, not a dark-theme variant.
- **CircleCiIcon** — Guidelines require Terminal (#161616) on light backgrounds and White on dark, and forbid any color not named in them.
- **CiscoIcon** — Cisco requires all parts of the mark be knocked out to white on dark backgrounds.
- **ClerkIcon** — Same two-tone symbol on light and dark; the mono symbol-dark/symbol-light pair is Clerk's alternate for single-colour contexts.
- **ClickhouseIcon** — Brand forbids recolouring the mark, so only its own published pair is used.
- **CloseIcon** — Close forbids modifying the logo, so the same colours are kept on light and dark.
- **CloudflareIcon** — Cloudflare's logo guidelines forbid altering the colours or filling the flare, so the flare stays knocked out on both themes.
- **CloudinaryIcon** — Cloudinary Blue is reserved for the logo; no other recolouring is permitted.
- **CockroachDbIcon** — The full-colour mark is a cyan-to-purple gradient; Cockroach Labs reduces it to solid white on dark backgrounds.
- **CoinbaseIcon** — Coinbase asks that the mark not be altered or recoloured, so only these two published variants are used.
- **ComapeoIcon** — Awana Digital publishes no reversed variant, so dark mode uses the brand's own accent blue #0066FF from CoMapeoLogo.svg (digidem/comapeo-mobile); the navy is 1.3:1 on dark surfaces.
- **ConfluenceIcon** — Atlassian requires the logo be used without modification, and its brand appearance is identical in light and dark.
- **ContentfulIcon** — Same full-colour mark on light and dark.
- **ContiguityIcon** — The `>_` glyph is knocked out to the opposite colour, so it carries its own fill- utilities.
- **ConvertKitIcon** — ConvertKit rebranded to Kit in 2024.
- **CssIcon** — Small-size variant; the only per-theme variants published are mono black/white fallbacks, so the rebeccapurple tile is kept in both themes.
- **DatadogIcon** — Datadog publishes one mark per background, the purple tile with Bits knocked out on light and the white Bits silhouette on dark, and forbids recolouring or inverting either.
- **DatoCmsIcon** — Brand kit forbids altering the logo's shape or colour.
- **DbtIcon** — Their Trademark Policy states "The dbt logo mark color cannot be altered", so this stays orange on both themes.
- **DeelIcon** — Post-rebrand the period is a square in the wordmark colour, not a blue circle.
- **DeepInfraIcon** — Their brand guidelines say "use the primary white logo on dark backgrounds", where the connector bars invert to #FFFFFF.
- **DenoIcon** — Deno publishes no hex and forbids colorizing; the black "Light (no outline)" and white "Dark (outlined)" marks are separate artworks to be swapped per background, never inverted.
- **DiscordIcon** — Discord forbids recolouring the logo, so no per-theme variant.
- **DiscourseIcon** — Only the outer bubble reverses; the five inner colours are the same in both variants.
- **DockerIcon** — Docker requires its logos appear only in its primary brand colours.
- **DropboxIcon** — Dropbox's branding terms forbid recolouring the logo, and their inverse-theme token keeps the same blue.
- **DuckDbIcon** — The pair is a full inversion, so the duck carries its own fill- utilities; the manual forbids recolouring outside these two brand hexes.
- **DustIcon** — Dust's guidelines forbid recolouring the logo.
- **DynatraceIcon** — Guidelines forbid colorizing the logo, so all six fills stay fixed in both themes.
- **EdgeDbIcon** — EdgeDB is now Gel — edgedb.com redirects to geldata.com — so this is Gel's "g" symbol, not the retired EDGE|DB wordmark.
- **EnodeIcon** — Their own favicon carries the pair in a prefers-color-scheme block.
- **ExaIcon** — Blue for standard applications, white on dark backgrounds.
- **FigmaIcon** — Figma's guidelines forbid modifying the marks, so the five-colour original is used on both themes.
- **FirebaseIcon** — Same full-colour artwork on light and dark; the guidelines forbid recolouring or redrawing the mark.
- **FoxentryIcon** — Fixed tri-tone mark, no per-theme variant: the brand ships a separate greyscale logo rather than a recoloured one.
- **FreshdeskIcon** — Freshworks publishes no reversed variant: the white glyph always sits on the green leaf.
- **FunkwhaleIcon** — Identity guidelines forbid recolouring.
- **GSheetsIcon** — Google forbids recolouring its marks, so the same full-colour artwork is used on both themes.
- **GcalIcon** — Google forbids modifying its logos, colour included, so this stays fixed with no per-theme pair.
- **GdriveIcon** — Google forbids modifying its logos "in any way, including changing the color", so this stays full-colour with no per-theme pair.
- **GiphyIcon** — Same full-colour mark on light and dark.
- **GitBookIcon**#1C1917 is the marketing palette's dark base, not the logomark.
- **GitIcon** — A white reversed logomark exists, but git-scm.com's own dark theme exempts the mark from inversion and keeps it orange.
- **GithubIcon** — GitHub allows the Invertocat in white or black only and forbids recolouring it, so the pair is fixed here rather than inherited from the caller.
- **GmailIcon** — Google's brand guidelines forbid recolouring the mark.
- **GoogleAiIcon** — Google ships no reversed variant; the same gradient is used on light and dark.
- **GoogleCalendarIcon** — Google's trademark guidelines forbid distorting or altering a brand feature, so no per-theme recolour.
- **GoogleCloudIcon** — Google forbids recolouring its logos.
- **GoogleDriveIcon** — Google's Drive branding guide permits resizing only — no other change to the logo — so no per-theme recolour.
- **GoogleFormsIcon** — Google's brand guidelines forbid modifying or recolouring its product icons.
- **GoogleIcon** — developers.google.com/identity/branding-guidelines forbids changing the colour of the G.
- **GorgiasIcon** — The guide allows only black or white for the symbol: "Do not use gray!".
- **GristIcon** — "Keep it exactly as depicted — no recoloring, no cropping."
- **GroqIcon** — Logo use in a UI requires a license from Groq.
- **HoldedIcon** — Holded ships one flat red mark for both themes; the red-orange gradient is retired.
- **HubspotIcon** — The legacy Coral #FF7A59 is not the current logo color.
- **IfsIcon** — IFS's negative lockup reverses only the wordmark, so the symbol keeps its #8427E2-to-#72C9F8 gradient on dark.
- **IftttIcon** — IFTTT's brand guidelines state "Our wordmark may be used in solid white or black" and publish no other hex for the mark.
- **IntercomIcon** — Intercom ships the mark as fill="currentColor" bound to its nav foreground token, so it takes the colour of the surface it sits on.
- **JavaIcon** — The Coffee Cup mark is licensee-only and "you may not use a modified version of the Coffee Cup logo" — do not recolour it or flatten it to currentColor.
- **JavaScriptIcon** — Fixed mark: yellow field, black lettering, no per-theme variant.
- **JiraIcon** — The logomark is identical on light and dark; only the wordmark changes colour.
- **JoomlaIcon** — Joomla's trademark policy forbids recolouring the mark, so there is no per-theme variant.
- **JotformIcon** — The other three bars keep their fixed brand colours in both themes.
- **JsonIcon** — JSON itself has no brand owner or published colours — json.org states none — so this is a Material palette pick, not a brand colour.
- **KanidmIcon** — Kanidm's artwork is CC-BY-NC-ND — no recolouring or other derivatives.
- **KlaviyoIcon** — Klaviyo draws it in currentColor, hence the white swap on dark.
- **LangfuseIcon** — Langfuse's trademark terms forbid modifying the assets.
- **LineIcon** — LINE forbids any change to the logo's colour, so there is no reversed variant.
- **LinearIcon** — Guidelines ship a light/dark logomark pair and forbid altering the assets in any other way.
- **LinkedinIcon** — That page forbids recolouring: only the approved blue, black and white variants.
- **LinodeIcon** — The keyline path stays unfilled so it follows currentColor instead of the source's near-black #231f20.
- **LumaAiIcon** — The two faces ship at 65% opacity, which is what makes their overlap read as a cube.
- **MSSqlServerIcon** — Microsoft licenses its product icons for diagrams, docs, and training only, and forbids cropping, rotating, or reshaping them.
- **MSTeamsIcon** — Microsoft's trademark guidelines forbid altering their brand assets, so the full-colour mark ships unchanged in both themes.
- **MailchimpIcon** — Mailchimp forbids altering the files, so both official tones are painted and neither is recoloured per theme.
- **MailerLiteIcon** — Their IP guidelines forbid altering or recolouring the mark.
- **MailgunIcon** — Mailgun ships no reversed variant; the tile is identical on light and dark.
- **MandrillIcon** — On dark their rule is the reversed (white) Freddie; Cavendish Yellow #FFE01B is a background colour, never the mark.
- **MarkdownIcon** — Spec: keep the enclosure's aspect ratio and radius, keep the M/arrow/box relative sizes, and draw all three in one colour.
- **MastodonIcon** — Swap to the black or white logo rather than recolouring when contrast fails.
- **MatrixIcon** — Artwork is the Foundation's matrix-icon.svg verbatim; the trademark policy forbids altering it.
- **MediumIcon** — Guidelines mandate black or white only for both the wordmark and the icon and forbid "any other colors, gradients, or filled with images".
- **MezmoIcon** — The star stays #F4B811 in both themes.
- **MicrosoftIcon** — Microsoft forbids recolouring the symbol, so it stays full-colour on both themes.
- **MistralIcon** — Brand forbids any other recolouring.
- **MixpanelIcon** — "The Mixpanel logo is only ever used in three colors: black, white and the primary brand purple."
- **MollieIcon** — The m is the glyph from Mollie-Logo-Black-2023.svg (Mollie logo pack, mollie.com/resources), scaled and placed to match that icon pixel for pixel; the logo pack itself ships only the 320x94 wordmark. Mollie publishes black and white variants, so the pair flips for dark mode.
- **MondayIcon** — All three colours are required; the brand forbids monochrome or recoloured versions, so no dark-theme variant.
- **MongodbIcon** — MongoDB permits only four logo colours, chosen for contrast with the background, and forbids any other recolour.
- **MotimateIcon** — Motimate is a registered trademark of Motimate AS (Kahoot!).
- **Mysql** — Used under Fair Use: https://fr.wikipedia.org/wiki/Fichier:MySQL.svg
- **NeonDbIcon** — Neon forbids recolouring, so only these published variants may be used.
- **NetlifyIcon** — Two colours per theme, so the "n" carries its own fill- utilities: #014847 on light, #FFFFFF on dark.
- **NetsuiteIcon** — Pre-Oracle NetSuite "N" mark. #125580/#baccdb approximate netsuite.com's own 2014 logo art, which is itself inconsistent: /portal/common/img/ns-logo.png is #14487e/#b9c9d5 and /portal/common/img/logo-ns-mobile.png is #13527d/#b6c7d5 (both via web.archive.org/web/2014/). Not Oracle's current NetSuite mark, which is a different logo in a different palette (#264759/#36677D/#94BFCE/#E2C06B).
- **NocoDbIcon** — NocoDB publishes no reversed variant; the full-colour mark is used on light and dark alike.
- **NotionIcon** — The plate is fixed, not theme-swapped: the mark is pure black and disappears on dark backgrounds without it.
- **NuIcon** — Nushell registers that one file as both the `light` and `dark` icon, so the green is not theme-swapped.
- **OdkIcon** — ODK publishes no reversed or monochrome variant.
- **OktaIcon** — Okta's official April-2025 logo package (logos-04-2025.zip) ships the mark in Black and White only.
- **OpenWeatherIcon** — Their negative (dark-background) logo reverses only the wordmark; the symbol stays brand orange.
- **OpenaiIcon** — The guidelines state "DON'T add any colors to the Blossom" — black or white only.
- **OracleDBIcon** — Oracle reserves its logo for licensees.
- **PHPIcon** — Official logo, CC BY-SA 4.0: keep it verbatim and credit Colin Viebrock rather than recolouring.
- **PaychexIcon** — The isolated P is the square mark Paychex ships as its own 192x192 app icon. Paychex requires prior approval for any use of its marks.
- **PaypalIcon** — The third fill is the deep/bright blue overlap: a two-colour or flat fill loses it.
- **PersonioIcon** — Black on light, white on dark or coloured backgrounds.
- **PhraseIcon** — The green wedge stays #03EAB3 in both — Phrase forbids altering the logo mark colour.
- **PineconeIcon** — The mark is stroke-only, so fill must stay none.
- **PinterestIcon** — Brand guidelines: "Do not alter the logo colour."
- **PipedriveIcon** — Pipedrive's partner media kit says "do not alter, rotate, modify or animate the logo", so the mark keeps its published colours on both themes.
- **PostgresIcon** — The PostgreSQL trademark policy forbids recolouring the mark without prior approval.
- **PostmarkIcon** — Postmark publishes no reversed variant.
- **PowershellIcon** — Trademarked Microsoft logo, exempt from that repo's MIT license. The #00FF18 line below is opacity-0 in the upstream asset and paints nothing.
- **PusherIcon** — Only the colourways shown in their brand guidelines are permitted.
- **PushoverIcon** — Forbids recolouring.
- **QoveryIcon** — The mark keeps the same purple in Qovery's white lockup for dark backgrounds, so there is no reversed variant.
- **QuickbooksIcon** — Intuit forbids altering the mark.
- **RIcon** — R Foundation licenses the mark CC-BY-SA 4.0 / GPL-2 — attribution required, changes must be indicated.
- **RaindropIcon** — Full-colour mark, no reversed variant published.
- **ReadwiseIcon** — The serif R and its highlight block are knocked out to the opposite colour, so they carry their own fill- utilities. Do not restore the mix-blend-mode: multiply wrapper Readwise's dark file carries: it turns the knocked-out white to the backdrop colour on anything but a white page.
- **RecraftIcon** — The plated mark carries its own background: recraft.ai serves it to prefers-color-scheme light and dark alike — not a theme pair.
- **RedditIcon** — Reddit publishes no reversed variant; the icon must always appear in Orangered when in colour.
- **RenderIcon** — Official Render Brand Kit contains only Black and White logomark folders and the SVGs use pure black / pure white.
- **ResendIcon** — Brand guidelines forbid multi-color use or altering the mark, so these are the only two published fills.
- **RssIcon** — Never rotate or flip the mark.
- **RubyIcon** — CC BY-SA 2.5; the kit's LICENSE asks that the mark not represent anything other than the Ruby language.
- **RunPodIcon** — Brand forbids recolouring, so both values are its own published cube-icon variants.
- **RustIcon** — rust-lang.org ships only rust-logo-blk.svg (pure black).
- **S3Icon** — AWS ships no dark variant for service icons.
- **SageIcon** — Sage sets its logo black on light surfaces and Sage green only on dark ones.
- **SalesforceIcon** — Salesforce reserves the white/reversed cloud for its own blue backgrounds, so the blue mark stands in both themes.
- **SegmentIcon** — Segment ships the mark in one flat green and publishes no reversed variant.
- **SendflakeIcon** — Snowflake Blue is the only approved logo color; the sole alternate is a white reverse reserved for full-bleed Snowflake Blue.
- **SendgridIcon** — Twilio's trademark guidelines forbid recolouring the mark, so it stays multicolour in both themes.
- **ServiceNowIcon** — Trademark guidelines require the mark in the graphic form provided.
- **ShopifyIcon** — The "S" stays white regardless of background; no gradients, shadows or recolouring.
- **ShortcutIcon** — The mark "is used across various colors but never changes its visual structure."
- **ShutterstockIcon** — Brand rule: the logo is only ever black or white.
- **SigNozIcon** — SigNoz ships no reversed variant; the tile mark is used unchanged on light and dark.
- **Slack** — Fixed full-colour mark, no per-theme variant.
- **SmartsheetIcon** — Those are two of the approved logo colorways; the guide forbids any other recolouring.
- **SnowflakeIcon** — Snowflake Blue is the only approved logo color; the sole alternate is a white reverse reserved for full-bleed Snowflake Blue.
- **SpeechifyIcon** — The blue logomark is reserved for white backgrounds; every other background takes the black or white monochrome version.
- **SplitwiseIcon** — Splitwise's logos carry a single green and no reversed variant, so the same colour is used on light and dark.
- **SpotifyIcon** — Spotify permits the green icon only on black or white backgrounds and requires the white monochrome colourway on any other dark background, so the pair is fixed rather than caller-set.
- **SquareIcon** — Square ships only black and white logo files and states "Do not change the color", so no tinted variant is allowed.
- **StraleIcon** — Strale's own logo component fills with currentColor, so the mark is meant to take the theme's foreground.
- **StravaIcon** — Strava's guidelines forbid modifying or altering its logos, and the orange Echelon is the mark Strava itself uses on light and dark alike.
- **StripeIcon** — Stripe's Marks Usage Terms forbid altering the marks, so this ships verbatim in both themes rather than as a recoloured pair.
- **SupabaseIcon** — Forbids modifying or recolouring the mark.
- **SurrealdbIcon** — Same gradient mark on light and dark; monochrome variants are for subtle placements only.
- **SvelteIcon** — Its guidelines count the official colour scheme as part of the mark — do not recolour.
- **TaskadeIcon** — Brand forbids recolouring, so only those two published variants are used.
- **TelegramIcon** — The shaded-plane drawing is Telegram's retired Logo_old.
- **TerraIcon** — The outlines are part of the artwork and stay black in both themes; the blue T carries the mark on dark backgrounds.
- **TheirStackIcon** — The mark ships black-only, but the same brand rules forbid placing it on low-contrast backgrounds; on a dark surface black is 1.68:1, so it is tinted to the brand's own white.
- **ThreadsIcon** — The pack ships the mark in black and white only, so it must never be tinted.
- **TogglIcon** — Toggl requires the mark be used as is, unmodified.
- **TomorrowIoIcon** — Their stylesheet reverses only the wordmark on dark headers (path.logo-letter{fill:#fff}); the mark itself stays logo blue in both themes.
- **TrelloIcon** — Atlassian: never compose your own versions or deconstruct official assets.
- **TripadvisorIcon** — Dark backgrounds take Tripadvisor's separate outlined Ollie, never an inverted or recoloured one.
- **TwilioIcon** — Twilio reserves its corporate logo for permitted use and forbids recreating or modifying it.
- **TwitchIcon** — Trademark guidelines forbid recolouring.
- **TwitterIcon** — The component draws the X glyph, not the legacy bird.
- **TypeformIcon** — Default brand colours are "Paper (white) and Ink (black)".
- **UltravoxIcon** — Gradient mark; ultravox.ai links that same asset for both prefers-color-scheme light and dark, so there is no per-theme variant.
- **VectaraIcon**#7E00FF#07FEEE iridescent sweep, sampled from the logo mark on vectara.com (no brand kit is published). Vectara reserves its trademarks: do not recolour.
- **VercelIcon** — Vercel ships only light-theme (black) and dark-theme (white) triangle marks and explicitly forbids modifying or recoloring the trademarks.
- **VismaIcon** — Positive black on light, negative white on dark; the logo may not be given any other colour.
- **VueIcon** — Vue's dark-background variant is separate outlined artwork rather than a recolour, so the two-tone mark is used in both themes.
- **WebflowIcon** — Mark ships in blue, black or white only.
- **WhatsappBusinessIcon** — "You shouldn't modify any colors in our logos."
- **WooCommerceIcon** — Automattic requires the mark in its exact, most up-to-date form, so only their two published colorways are used, never a recolour.
- **WordpressIcon** — Every official logotype vector is BaseGray #32373C, shipped alongside a White/transparent version for dark backgrounds.
- **XataIcon** — Brand forbids recolouring, and the full-colour symbol is the same purple in light and dark modes.
- **XeroIcon** — Single-colour mark: white wordmark on the blue badge in both themes.
- **YamlIcon** — YAML publishes no reversed variant, and its black letters would be invisible on the dark surface.
- **YelpIcon** — Yelp forbids altering the logos and requires the ® to accompany the mark at all times.
- **YoutubeIcon** — "The triangle in the full-color red icon must always be white."
- **ZendeskIcon** — Zendesk's brand guidelines specify the logo in Licorice #11110D and Coconut #FFFFFF only and forbid unapproved color variations.
- **ZeroTierIcon** — The tile is identical on light and dark; only the wordmark inverts.
- **ZitadelIcon** — The gradient chevrons stay #FF8F00#FE00FF in both variants.
- **ZohoIcon** — The four squares carry their own brand hexes (#E42527/#089949/#226DB4/#F9B21D) on light; Zoho's reversed lock-up is entirely white, so they invert with the wordmark on dark.
- **ZoomIcon** — The logo "may only be used in Bloom (#0B5CFF), White, or Black", with White reserved for dark backgrounds and Black requiring prior brand approval.
- **ZuploIcon** — Brand guidelines forbid recolouring the mark; pink is the official variant on both light and dark surfaces.
## Concept icons
Not brands. These inherit `currentColor` on purpose and must not be given a pair.
`AgentInstructionsIcon`, `AiAgentIcon`, `ApiKeyAuthIcon`, `AssetDatabaseIcon`, `AssetDucklakeIcon`, `AssetGenericIcon`, `AssetResIcon`, `AssetS3Icon`, `BarsStaggered`, `BasicHttpAuthIcon`, `BcryptIcon`, `CACertificate`, `CustomAiIcon`, `DbIcon`, `FormInputIcon`, `FunnelCog`, `GpgKeyIcon`, `HttpIcon`, `JsonSchemaIcon`, `LdapIcon`, `Mail`, `OauthIcon`, `PaintbrushOff`, `QRCodeIcon`, `QuestionInputIcon`, `RecordIcon`, `RestIcon`, `SchedulePollIcon`, `SignatureAuthIcon`, `SparklesOffIcon`, `WebdavIcon`, `WindmillAiIcon`, `WindmillIcon`, `WindmillIcon2`
## Coverage
- brand icons: **314**, of which **310** carry a recorded source
- per-theme pairs applied: **136** (5 of them by inversion or a two-SVG swap, see above)
- concept icons: **34**
- effectively invisible on light: **1** (AbstractApiIcon)
- effectively invisible on dark: **2** (PaychexIcon, TripadvisorIcon)
@@ -1,12 +1,27 @@
<script lang="ts">
interface Props {
height?: string;
width?: string;
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props();
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg {width} {height} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M21.7142 13.6433h-4.9888a.651.651 0 00-.655.555 4.1139 4.1139 0 01-4.0619 3.5299l1.35 6.1728a10.3737 10.3737 0 009.0077-9.5447.651.651 0 00-.652-.713zm-8.6327-.158l7.1998-6.1718a.645.645 0 000-.984L13.0815.1597a.648.648 0 00-1.074.483v12.3426a.651.651 0 001.073.5zm-11.3547 1.505A10.3847 10.3847 0 0012.0115 24v-6.2698a4.0929 4.0929 0 01-4.0999-4.0869zm-.096-1.447v.1h6.2798a4.0929 4.0929 0 014.098-4.0879l-1.348-6.1698a10.3697 10.3697 0 00-9.0298 10.1577"/>
<!-- #599D15 / #FFFFFF per bamboohr.com (Encore --brandColor; bamboohr-logo-white.png is the published reversed variant). -->
<svg
class="text-[#599D15] dark:text-[#FFFFFF]"
{width}
{height}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<g transform="translate(-10.2991 -6.278) scale(0.2688)">
<path
d="M103.75,54.99c-8.52,0-13.08,2.92-16.28,6.1l-0.87,0.92l0-29.51h-7.36v47.67c0,14.37,11.07,23.33,23.77,23.33c13.99,0,24.59-10.77,24.59-24.61C127.6,66.04,116.55,54.99,103.75,54.99z M103.01,96.68c-9.27,0-17.12-7.31-17.12-17.1c0-9.8,6.61-17.84,17.28-17.84c10.67,0,16.95,8.63,16.95,17.66C120.13,89.23,113.48,96.68,103.01,96.68z"
/>
<path
d="M55.89,32.5c-0.06-0.02-0.09,0.05-0.05,0.1c7.55,8.52,13.02,18.83,15.76,25.44c-3.46-3.71-6.76-7.57-10.46-10.14c-7.5-5.23-15.42-7.82-22.76-8.8c-0.06-0.01-0.09,0.07-0.04,0.11c17.98,14.42,13.79,21.96,37.26,24.94c0.04,0.01,0.08-0.04,0.06-0.08c-3.47-10.69-4.6-18.62-10.49-24.92C63.31,37.17,57.85,33.18,55.89,32.5z"
/>
</g>
</svg>
@@ -1,12 +1,15 @@
<script lang="ts">
interface Props {
height?: string;
width?: string;
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props();
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg {width} {height} viewBox="0 0 24 24" fill="#6078FF" xmlns="http://www.w3.org/2000/svg">
<path d="M22.109 7.951l1.485 2.464a3.507 3.507 0 010 3.275l-4.505 7.717a3.333 3.333 0 01-2.94 1.793H7.83a3.335 3.335 0 01-2.94-1.793l-1.555-2.632 6.139-5.695 4.447 2.578a1.093 1.093 0 001.456-.198zm-13.39.628L1.99 16.15.406 13.725a3.495 3.495 0 010-3.27L5.158 2.59A3.338 3.338 0 018.1.8h8.008c1.228 0 2.357.687 2.942 1.79l1.616 2.722-6.017 5.592-4.432-2.574a1.098 1.098 0 00-1.499.248z"/>
<!-- #5386FF per the mark in baremetrics.com's header logo (baremetrics-logo.svg), the asset this path is taken from. -->
<svg {width} {height} viewBox="0 0 24 24" fill="#5386FF" xmlns="http://www.w3.org/2000/svg">
<path
d="M22.109 7.951l1.485 2.464a3.507 3.507 0 010 3.275l-4.505 7.717a3.333 3.333 0 01-2.94 1.793H7.83a3.335 3.335 0 01-2.94-1.793l-1.555-2.632 6.139-5.695 4.447 2.578a1.093 1.093 0 001.456-.198zm-13.39.628L1.99 16.15.406 13.725a3.495 3.495 0 010-3.27L5.158 2.59A3.338 3.338 0 018.1.8h8.008c1.228 0 2.357.687 2.942 1.79l1.616 2.722-6.017 5.592-4.432-2.574a1.098 1.098 0 00-1.499.248z"
/>
</svg>
@@ -0,0 +1,24 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<!-- #2BC3F1 / #5190EF / #4D68C4 per the baserow.io favicon and horizontal logo. The mark keeps these three colours on light and dark; only the wordmark reverses to white. -->
<svg {width} {height} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill="#2BC3F1"
d="M1.291.068A1.29 1.29 0 0 0 0 1.36v3.352a1.29 1.29 0 0 0 1.291 1.291h3.354a1.29 1.29 0 0 0 1.289-1.291V1.359A1.29 1.29 0 0 0 4.644.07Zm9.033 0a1.29 1.29 0 0 0-1.29 1.291v3.352a1.29 1.29 0 0 0 1.29 1.291H22.71A1.29 1.29 0 0 0 24 4.711V1.359A1.29 1.29 0 0 0 22.709.07Z"
/>
<path
fill="#5190EF"
d="M1.291 9.033A1.29 1.29 0 0 0 0 10.323v3.353a1.29 1.29 0 0 0 1.291 1.29h21.418A1.29 1.29 0 0 0 24 13.677v-3.354a1.29 1.29 0 0 0-1.291-1.289Z"
/>
<path
fill="#4D68C4"
d="M1.291 17.998A1.29 1.29 0 0 0 0 19.289v3.352a1.29 1.29 0 0 0 1.291 1.29h12.385a1.29 1.29 0 0 0 1.29-1.29v-3.352a1.29 1.29 0 0 0-1.29-1.291zm18.064 0a1.29 1.29 0 0 0-1.289 1.291v3.352a1.29 1.29 0 0 0 1.29 1.29h3.353A1.29 1.29 0 0 0 24 22.642v-3.352a1.29 1.29 0 0 0-1.291-1.291z"
/>
</svg>
@@ -0,0 +1,25 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
{width}
{height}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="10" cy="7" r="4" />
<path d="M10.3 15H7a4 4 0 0 0-4 4v2" />
<path d="M15 15.5V14a2 2 0 0 1 4 0v1.5" />
<rect width="8" height="5" x="13" y="16" rx=".899" />
</svg>
@@ -0,0 +1,26 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<!-- #1D2032 / #EBEDFF per developers.basistheory.com/img/bt-logo-light.svg and bt-logo-dark.svg,
which ship the same mark geometry in the two theme colours. -->
<svg
class="text-[#1D2032] dark:text-[#EBEDFF]"
{width}
{height}
viewBox="0 6.2 27.5 27.5"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M26,6.2H1.4c-.8,0-1.4.6-1.4,1.4v5.1c0,.8.6,1.4,1.4,1.4h10.4c1.1,0,2.2.5,3,1.3l3.7,3.8c.7.8,1.1,1.8,1.1,2.8v10.2c0,.8.6,1.4,1.4,1.4h5.1c.8,0,1.4-.6,1.4-1.4V7.6c0-.8-.6-1.4-1.4-1.4Z"
/>
<path
d="M11.6,20c-.3-.3-.6-.4-1-.4H1.4c-.8,0-1.4.6-1.4,1.4v8.7c0,.4.1.7.4,1l2.6,2.6c.3.3.6.4,1,.4h8.7c.8,0,1.4-.6,1.4-1.4v-9.1c0-.4-.1-.7-.4-.9l-2.1-2.2Z"
/>
</svg>
@@ -1,15 +1,39 @@
<script lang="ts">
interface Props {
height?: string;
width?: string;
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props();
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg {width} {height} viewBox="0 0 168 168" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M28 63H140" stroke="black" stroke-width="14" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M28 105H140" stroke="black" stroke-width="14" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M70 21L56 147" stroke="black" stroke-width="14" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M112 21L98 147" stroke="black" stroke-width="14" stroke-linecap="round" stroke-linejoin="round"/>
<path
d="M28 63H140"
stroke="currentColor"
stroke-width="14"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M28 105H140"
stroke="currentColor"
stroke-width="14"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M70 21L56 147"
stroke="currentColor"
stroke-width="14"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M112 21L98 147"
stroke="currentColor"
stroke-width="14"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
@@ -0,0 +1,35 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<!-- #1C1E21 / #FFFFFF per the getbeamer.com header logo (g#isotype) and their webclip app icon,
which sets the same mark in white on #1C1E21. The isotype is monochrome in all first-party artwork. -->
<svg
class="text-[#1C1E21] dark:text-[#FFFFFF]"
{width}
{height}
viewBox="0 0 42 42"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-label="Beamer"
>
<g transform="translate(1.66 -3.89)" fill="currentColor">
<path
d="M3.3,39.3c-1.1,0-2.1-0.6-2.5-1.7c-0.6-1.4,0.1-3,1.5-3.5l32-13.1c1.4-0.6,3,0.1,3.6,1.5c0.6,1.4-0.1,3-1.5,3.5l-32,13.1 C4,39.2,3.7,39.3,3.3,39.3z"
/>
<path
d="M3.3,28.9c-1.1,0-2.1-0.6-2.5-1.7c-0.6-1.4,0.1-3,1.5-3.5l32-13.1c1.4-0.6,3,0.1,3.6,1.5c0.6,1.4-0.1,3-1.5,3.5l-32,13.1 C4,28.9,3.7,28.9,3.3,28.9z"
/>
<path
d="M3.3,18.2c-1.1,0-2.1-0.6-2.5-1.7c-0.6-1.4,0.1-3,1.5-3.5l19.6-7.9c1.4-0.6,3,0.1,3.6,1.5c0.6,1.4-0.1,3-1.5,3.5L4.4,18 C4,18.2,3.7,18.2,3.3,18.2z"
/>
<path
d="M15.6,44.9c-1.1,0-2.1-0.6-2.5-1.7c-0.6-1.4,0.1-3,1.5-3.5l19.6-7.9c1.4-0.6,3,0.1,3.6,1.5c0.6,1.4-0.1,3-1.5,3.5 l-19.6,7.9C16.3,44.8,16,44.9,15.6,44.9z"
/>
</g>
</svg>
@@ -1,42 +1,36 @@
<!-- Four-colour mark per Google Cloud's official icon library (cloud.google.com/icons, core-products-icons.zip). Google publishes no reversed variant, so the same mark is used on both themes. -->
<script lang="ts">
interface Props {
height?: string;
width?: string;
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props();
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg xmlns="http://www.w3.org/2000/svg" {width} {height} viewBox="0 0 24 24"
><defs
><style>
.cls-1 {
fill: #aecbfa;
}
.cls-1,
.cls-2,
.cls-3 {
fill-rule: evenodd;
}
.cls-2 {
fill: #669df6;
}
.cls-3 {
fill: #4285f4;
}
</style></defs
><title>Icon_24px_BigQuery_Color</title><g data-name="Product Icons"
><g
><path class="cls-1" d="M6.73,10.83v2.63A4.91,4.91,0,0,0,8.44,15.2V10.83Z" /><path
class="cls-2"
d="M9.89,8.41v7.53A7.62,7.62,0,0,0,11,16,8,8,0,0,0,12,16V8.41Z"
/><path class="cls-1" d="M13.64,11.86v3.29a5,5,0,0,0,1.7-1.82V11.86Z" /><path
class="cls-3"
d="M17.74,16.32l-1.42,1.42a.42.42,0,0,0,0,.6l3.54,3.54a.42.42,0,0,0,.59,0l1.43-1.43a.42.42,0,0,0,0-.59l-3.54-3.54a.42.42,0,0,0-.6,0"
/><path
class="cls-2"
d="M11,2a9,9,0,1,0,9,9,9,9,0,0,0-9-9m0,15.69A6.68,6.68,0,1,1,17.69,11,6.68,6.68,0,0,1,11,17.69"
/></g
></g
></svg
>
<svg xmlns="http://www.w3.org/2000/svg" {width} {height} viewBox="0 0 512 512">
<path
fill="#34a853"
d="M311.9,418.9c-8.8,0-16-7.2-16-16v-145.8c0-8.8,7.2-16,16-16s16,7.2,16,16v145.8c0,8.8-7.2,16-16,16h0Z"
/>
<path
fill="#34a853"
d="M147.6,418.9c-8.8,0-16-7.2-16-16v-200.5c0-8.8,7.2-16,16-16s16,7.2,16,16v200.5c0,8.8-7.2,16-16,16Z"
/>
<path
fill="#34a853"
d="M229.8,437.4c-8.8,0-16-7.2-16-16V147.6c0-8.8,7.2-16,16-16s16,7.2,16,16v273.7c0,8.8-7.2,16-16,16h0Z"
/>
<path
fill="#fbbc04"
d="M229.8,437.4c-114.5,0-207.6-93.1-207.6-207.6h32c0,96.8,78.8,175.6,175.6,175.6s175.6-78.8,175.6-175.6h32c0,114.5-93.1,207.6-207.6,207.6h0Z"
/>
<path
fill="#ea4335"
d="M437.4,229.8h-32c0-96.8-78.8-175.6-175.6-175.6S54.1,132.9,54.1,229.8H22.1c0-114.5,93.2-207.7,207.7-207.7s207.6,93.1,207.6,207.6h0Z"
/>
<path
fill="#4285f4"
d="M487.4,464.8l-100-100c32.3-37.6,49.9-85,49.9-135.1s-21.6-107.6-60.8-146.8l-22.6,22.6c33.2,33.2,51.4,77.3,51.4,124.2s-18.3,90.9-51.5,124.1h0c-5,5-7.5,7-11.8,10.8l122.8,122.8c3.1,3.1,7.2,4.7,11.3,4.7h0c4.1,0,8.2-1.6,11.3-4.7,6.2-6.2,6.2-16.4,0-22.6h0Z"
/>
</svg>
@@ -1,24 +1,23 @@
<script lang="ts">
interface Props {
height?: string;
width?: string;
}
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props();
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" {width} {height} viewBox="0 0 62.42 62.42">
<defs>
<linearGradient id="New_Gradient_Swatch_1" x1="64.01" y1="30.27" x2="32.99" y2="54.48" gradientUnits="userSpaceOnUse">
<stop offset="0.18" stop-color="#0052cc" />
<stop offset="1" stop-color="#2684ff" />
</linearGradient>
</defs>
<title>Bitbucket-blue</title>
<g id="Layer_2" data-name="Layer 2" >
<g id="Blue" transform="translate(0 -3.13)">
<path d="M2,6.26A2,2,0,0,0,0,8.58L8.49,60.12a2.72,2.72,0,0,0,2.66,2.27H51.88a2,2,0,0,0,2-1.68L62.37,8.59a2,2,0,0,0-2-2.32ZM37.75,43.51h-13L21.23,25.12H40.9Z" fill="#2684ff" />
<path d="M59.67,25.12H40.9L37.75,43.51h-13L9.4,61.73a2.71,2.71,0,0,0,1.75.66H51.89a2,2,0,0,0,2-1.68Z" fill="url(#New_Gradient_Swatch_1)"/>
</g>
</g>
</svg>
<!-- #1868DB / #FFFFFF per atlassian.design/foundations/logos (Bitbucket mark, brand and inverse).
Atlassian ships brand/neutral/inverse only: "don't use unapproved color combinations". -->
<svg
class="text-[#1868DB] dark:text-[#FFFFFF]"
{width}
{height}
viewBox="0 0 48 48"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M38.3789 23.417L35.9515 38.2453C35.7932 39.1424 35.16 39.6701 34.2629 39.6701H13.6827C12.7856 39.6701 12.1523 39.1424 11.994 38.2453L7.71967 11.8076C7.56136 10.9105 8.03629 10.3301 8.88061 10.3301H39.0649C39.9092 10.3301 40.3842 10.9105 40.2259 11.8076L39.0649 18.7732C38.9066 19.7759 38.3262 20.198 37.3763 20.198H19.2235C18.9596 20.198 18.8013 20.3563 18.8541 20.673L20.2789 29.4327C20.3317 29.6438 20.49 29.8021 20.701 29.8021H27.2445C27.4556 29.8021 27.6139 29.6438 27.6667 29.4327L28.6693 23.1004C28.7748 22.3088 29.3025 21.9922 30.0413 21.9922H37.1652C38.2206 21.9922 38.5372 22.5199 38.3789 23.417Z"
/>
</svg>
@@ -1,12 +1,21 @@
<!-- #F36600 on both themes per bitly.com/pages/bitly-logo-usage-guidelines-for-media (Bitly-MediaKit glyph_bitly_orange_RGB.svg). Bitly's reversed logomark is white over orange, not over neutral dark, so the orange mark is used on both. -->
<script lang="ts">
interface Props {
height?: string;
width?: string;
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props();
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg {width} {height} viewBox="0 0 24 24" fill="#EE6123" xmlns="http://www.w3.org/2000/svg">
<path d="M13.055 21.26c-1.345.022-2.325-.41-2.386-1.585-.025-.44-.018-.91.002-1.192.137-1.716 1.333-2.95 2.53-3.19 1.482-.294 2.455.38 2.455 2.31 0 1.303-.36 3.618-2.59 3.657h-.016zM11.923 0C5.32 0 0 5.297 0 12.224c0 3.594 1.92 7.062 4.623 9.147.52.4 1.138.367 1.497.02.297-.285.272-.984-.285-1.475-2.16-1.886-3.652-4.76-3.652-7.635 0-5.15 4.58-9.49 9.74-9.49 6.28 0 9.636 5.102 9.636 9.43 0 2.65-1.29 5.84-3.626 7.874.015 0 .493-.942.493-2.784 0-3.13-1.976-4.836-4.28-4.836-1.663 0-2.667.598-3.34 1.152 0-1.272.045-3.652.045-3.652 0-1.572-.54-2.83-2.47-2.86-1.11-.015-1.932.493-2.44 1.647-.18.436-.12.916.254 1.125.3.18.81.046 1.046-.284.165-.21.254-.254.404-.24.24.03.257.405.257.66.014.193.193 2.903.088 9.865C7.98 21.798 9.493 24 13.1 24c1.56 0 2.756-.435 4.493-1.422C20.243 21.08 24 17.758 24 12.128 23.953 5.045 18.265 0 11.933 0"/>
<svg
{width}
{height}
viewBox="0 0 841.88983154 841.88976378"
fill="#F36600"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M140.09621293,735.35737312C46.69873176,645.31122751,0,542.58096432,0,427.16654822,0,180.54758592,191.57368493,0,419.4983359,0c230.29012095,0,422.39142788,180.54760358,422.39142788,427.16654822,0,233.10272761-205.96568737,413.47182614-385.84964913,413.47182614-74.50545286,0-128.32600445-28.13433058-156.47188338-71.94482632-9.96137265-15.50537668-18.84106723-40.50251065-20.6344876-55.65190141-1.19560769-10.09961738-1.19560769-43.6096886,0-100.53021367v-127.1354293c1.05967616-76.78983715,9.26599935-178.38447699-21.22617293-154.96540603-9.04237803,8.71300276-12.68178672,17.77146725-32.04494899,18.59991148-19.37725337-1.41147713-24.82620422-22.14288967-21.6244675-33.29630309,11.49543184-40.02969918,45.29899721-72.09576718,99.00461267-66.30744661,53.70563311,5.78832057,74.36129269,41.2911007,75.7808572,89.22075622,0,57.95447347-1.41956451,121.1901818-1.41956451,128.53850121v6.20640979c.26472922,5.75842552-.57084886,10.41530768,6.21993584,6.55670983,6.94578691-4.7161072,12.93613302-8.80274088,17.97100301-12.25986571,36.75939633-27.81687448,126.96499397-57.15961482,196.97075648,12.25986571,26.64180307,26.41867618,59.4444229,83.78008699,40.34485974,172.29429237-3.06896597,9.27647056-.61117981,10.50448074,7.3733585,3.68381865,6.59435675-6.5621485,96.96950682-87.85534889,96.96950682-241.69035834,0-172.44235497-151.42193342-316.40251613-323.7551441-316.40251613-150.16861928,0-322.02246736,119.36925022-322.02246736,329.35203394,0,65.23812916,23.41495664,145.92922597,80.73926792,208.94710482,9.36403674,10.73989762,16.46122362,18.28362081,21.29156063,22.63116957,26.15595998,22.46144057,29.59788125,50.73187925,12.21898736,71.27586943-22.34035944,22.50608007-46.21684406,24.28495022-71.62945387,5.33668108ZM457.13035686,744.89497066c78.0118436-1.25809947,90.59439219-82.19616151,91.01380577-127.90736783,0-67.51831084-34.39230182-91.42227136-85.98077221-80.93806204-41.52243858,8.38735333-83.46425543,51.58229008-88.07787541,111.55193348-.83882715,10.06486637-1.25824073,26.42023009-.41941358,41.93683728,2.0971032,41.09808076,36.48940502,56.195345,83.46425543,55.35658848Z"
/>
</svg>
@@ -1,12 +1,24 @@
<!-- #F57C00 per Google's Blogger product logo (gstatic.com/images/branding/productlogos/blogger/v5/192px.svg). Google publishes no reversed variant. -->
<script lang="ts">
interface Props {
height?: string;
width?: string;
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props();
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg {width} {height} viewBox="0 0 24 24" fill="#FF5722" xmlns="http://www.w3.org/2000/svg">
<path d="M21.976 24H2.026C.9 24 0 23.1 0 21.976V2.026C0 .9.9 0 2.025 0H22.05C23.1 0 24 .9 24 2.025v19.95C24 23.1 23.1 24 21.976 24zM12 3.975H9c-2.775 0-5.025 2.25-5.025 5.025v6c0 2.774 2.25 5.024 5.025 5.024h6c2.774 0 5.024-2.25 5.024-5.024v-3.975c0-.6-.45-1.05-1.05-1.05H18c-.524 0-.976-.45-.976-.976 0-2.776-2.25-5.026-5.024-5.026zm3.074 12H9c-.525 0-.975-.45-.975-.975s.45-.976.975-.976h6.074c.526 0 .977.45.977.976s-.45.976-.975.976zm-2.55-7.95c.527 0 .976.45.976.975s-.45.975-.975.975h-3.6c-.525 0-.976-.45-.976-.975s.45-.975.975-.975h3.6z"/>
<svg {width} {height} viewBox="0 0 192 192" xmlns="http://www.w3.org/2000/svg">
<path
d="M172,125c0,25.96-21.04,47-47,47H67c-25.96,0-47-21.04-47-47V67c0-25.96,21.04-47,47-47h58 c25.96,0,47,21.04,47,47V125z"
fill="#F57C00"
/>
<path
d="M134,89h-2.84c-3.22,0-6.16-2.78-6.16-6c0-15-11.51-27-26.64-27H79.5C64.31,56,52,68.31,52,83.5l7.59,13.85 L52,112.6v3.9c0,15.19,12.31,27.5,27.5,27.5h33c15.19,0,27.5-12.31,27.5-27.5v-3.9l-6.62-8.47L140,95C140,91.7,137.3,89,134,89z M73,84L73,84c0-3.32,2.69-6,6-6H101c3.31,0,6,2.69,6,6v0c0,3.31-2.68,6-6,6H79C75.68,90,73,87.32,73,84z M119,117L119,117 c0,3.31-2.69,6-6,6H79c-3.31,0-6-2.69-6-6v0c0-3.31,2.69-6,6-6h34C116.31,111,119,113.69,119,117z"
opacity=".2"
/>
<path
d="M134,85h-2.84c-3.22,0-6.16-2.78-6.16-6c0-15-11.51-27-26.64-27H79.5C64.31,52,52,64.31,52,79.5v33 c0,15.19,12.31,27.5,27.5,27.5h33c15.19,0,27.5-12.31,27.5-27.5V91C140,87.7,137.3,85,134,85z M73,80L73,80c0-3.32,2.69-6,6-6H101 c3.31,0,6,2.69,6,6v0c0,3.31-2.68,6-6,6H79C75.68,86,73,83.32,73,80z M119,113L119,113c0,3.31-2.69,6-6,6H79c-3.31,0-6-2.69-6-6v0 c0-3.31,2.69-6,6-6h34C116.31,107,119,109.69,119,113z"
fill="#FFFFFF"
/>
</svg>
@@ -1,12 +1,25 @@
<script lang="ts">
interface Props {
height?: string;
width?: string;
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props();
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg {width} {height} viewBox="0 0 24 24" fill="#1185FE" xmlns="http://www.w3.org/2000/svg">
<path d="M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037-.856 3.061-3.978 3.842-6.755 3.37 4.854.826 6.089 3.562 3.422 6.299-5.065 5.196-7.28-1.304-7.847-2.97-.104-.305-.152-.448-.153-.327 0-.121-.05.022-.153.327-.568 1.666-2.782 8.166-7.847 2.97-2.667-2.737-1.432-5.473 3.422-6.3-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026"/>
<!-- Blue500 #0560FF / Neutral0 #FFFFFF per bsky.social/about/support/branding. The downloadable
media-kit butterfly still ships the older #006AFF, but the palette is the normative source:
"use only the official color values above. Do not substitute, tint, or approximate." White is
the approved monochrome variant for dark backgrounds. -->
<svg
class="text-[#0560FF] dark:text-[#FFFFFF]"
{width}
{height}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037-.856 3.061-3.978 3.842-6.755 3.37 4.854.826 6.089 3.562 3.422 6.299-5.065 5.196-7.28-1.304-7.847-2.97-.104-.305-.152-.448-.153-.327 0-.121-.05.022-.153.327-.568 1.666-2.782 8.166-7.847 2.97-2.667-2.737-1.432-5.473 3.422-6.3-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026"
/>
</svg>
@@ -0,0 +1,16 @@
<script lang="ts">
interface Props {
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props()
</script>
<!-- #A973FF per botify.com design tokens (--color--surface--purple-05). -->
<svg {width} {height} viewBox="0 0.632812 15.7288 21.951" xmlns="http://www.w3.org/2000/svg">
<path
d="M9.72505 18.0189C9.17514 18.3277 8.54237 18.5085 7.86441 18.5085C5.77024 18.5085 4.07533 16.8136 4.07533 14.7194C4.07533 12.6253 5.77024 10.9304 7.86441 10.9304C9.95857 10.9304 11.6535 12.6253 11.6535 14.7194C11.6535 15.3974 11.4727 16.0302 11.1638 16.5801L14.0942 19.5104C15.1186 18.1846 15.7288 16.5198 15.7288 14.7119C15.7288 10.3729 12.2109 6.90776 7.87194 6.90776C6.5838 6.90776 5.371 7.27688 4.29379 7.82679V0.632812H0V14.727C0 19.5405 3.51789 22.5838 7.85687 22.5838C9.66478 22.5838 11.322 21.9737 12.6554 20.9492L9.72505 18.0189Z"
fill="#A973FF"
/>
</svg>
@@ -1,12 +1,23 @@
<script lang="ts">
interface Props {
height?: string;
width?: string;
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props();
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg {width} {height} viewBox="0 0 24 24" fill="#0061D5" xmlns="http://www.w3.org/2000/svg">
<path d="M.959 5.523c-.54 0-.959.42-.959.899v7.549a4.59 4.59 0 004.613 4.494 4.717 4.717 0 004.135-2.457c.779 1.438 2.337 2.457 4.074 2.457 2.577 0 4.674-2.037 4.674-4.613.06-2.457-2.037-4.495-4.613-4.495-1.738 0-3.295.959-4.074 2.397-.78-1.438-2.338-2.397-4.135-2.397-1.079 0-2.038.36-2.817.899V6.422a.92.92 0 00-.898-.899zM17.602 9.26a.95.95 0 00-.704.158c-.36.3-.479.899-.18 1.318l2.397 3.116-2.396 3.115c-.3.42-.24.96.18 1.26.419.3 1.016.298 1.316-.122l2.039-2.636 2.096 2.697c.3.36.899.419 1.318.12.36-.3.42-.84.121-1.259l-2.338-3.115 2.338-3.057c.3-.419.298-1.018-.121-1.318-.48-.3-1.019-.24-1.318.18l-2.096 2.576-2.04-2.695c-.149-.18-.373-.3-.612-.338zM4.613 11.154c1.558 0 2.817 1.26 2.817 2.758 0 1.558-1.259 2.756-2.817 2.756-1.558 0-2.816-1.198-2.816-2.756 0-1.498 1.258-2.758 2.816-2.758zm8.27 0c1.558 0 2.816 1.26 2.816 2.758-.06 1.558-1.318 2.756-2.816 2.756-1.558 0-2.817-1.198-2.817-2.756 0-1.498 1.259-2.758 2.817-2.758Z"/>
<!-- #0061D5 / #FFFFFF per box.com (.box-logo-svg fill:#0061d5, reversed to #fff over the dark
masthead). box.com/legal/trademark forbids any other recolouring of the mark. -->
<svg
class="text-[#0061D5] dark:text-[#FFFFFF]"
{width}
{height}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M.959 5.523c-.54 0-.959.42-.959.899v7.549a4.59 4.59 0 004.613 4.494 4.717 4.717 0 004.135-2.457c.779 1.438 2.337 2.457 4.074 2.457 2.577 0 4.674-2.037 4.674-4.613.06-2.457-2.037-4.495-4.613-4.495-1.738 0-3.295.959-4.074 2.397-.78-1.438-2.338-2.397-4.135-2.397-1.079 0-2.038.36-2.817.899V6.422a.92.92 0 00-.898-.899zM17.602 9.26a.95.95 0 00-.704.158c-.36.3-.479.899-.18 1.318l2.397 3.116-2.396 3.115c-.3.42-.24.96.18 1.26.419.3 1.016.298 1.316-.122l2.039-2.636 2.096 2.697c.3.36.899.419 1.318.12.36-.3.42-.84.121-1.259l-2.338-3.115 2.338-3.057c.3-.419.298-1.018-.121-1.318-.48-.3-1.019-.24-1.318.18l-2.096 2.576-2.04-2.695c-.149-.18-.373-.3-.612-.338zM4.613 11.154c1.558 0 2.817 1.26 2.817 2.758 0 1.558-1.259 2.756-2.817 2.756-1.558 0-2.816-1.198-2.816-2.756 0-1.498 1.258-2.758 2.816-2.758zm8.27 0c1.558 0 2.816 1.26 2.816 2.758-.06 1.558-1.318 2.756-2.816 2.756-1.558 0-2.817-1.198-2.817-2.756 0-1.498 1.259-2.758 2.817-2.758Z"
/>
</svg>
@@ -0,0 +1,58 @@
<script lang="ts">
interface Props {
/** Usually the brand's initial. */
letter: string
/** Literal Tailwind fill classes for the rounded square behind the letter, e.g.
* "fill-transparent dark:fill-[#584CCC]". */
bgClass: string
/** Literal Tailwind text classes for the letter, e.g.
* "text-[#584CCC] dark:text-white". */
textClass: string
height?: string
width?: string
size?: number
}
let {
letter,
bgClass,
textClass,
height = '24px',
width = '24px',
size = undefined
}: Props = $props()
const w = $derived(size ? `${size}px` : width)
const h = $derived(size ? `${size}px` : height)
</script>
<!-- Stand-in for brands that reserve their logo for licensees: a letter in the brand's own
colour, rather than their mark or invented artwork.
On light the letter carries the colour on a bare ground. On dark that inverts to a filled
rounded square with a light letter, because a mid-tone brand colour chosen to read on white
goes dim as a foreground on #2e3441 — the filled tile puts the contrast back into the letter.
Both class props must be written as literal strings at the call site: Tailwind only emits
classes it can see while scanning, so building them from a variable produces no CSS. -->
<svg
width={w}
height={h}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-label={letter}
>
<rect width="24" height="24" rx="5" class={bgClass} />
<text
x="12"
y="12"
fill="currentColor"
class={textClass}
font-size="17"
font-weight="700"
font-family="ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif"
text-anchor="middle"
dominant-baseline="central">{letter}</text
>
</svg>
@@ -1,12 +1,20 @@
<script lang="ts">
interface Props {
height?: string;
width?: string;
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props();
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg {width} {height} viewBox="0 0 24 24" fill="#0B996E" xmlns="http://www.w3.org/2000/svg">
<path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zM7.2 4.8h5.747c2.34 0 3.895 1.406 3.895 3.516 0 1.022-.348 1.862-1.09 2.588C17.189 11.812 18 13.22 18 14.785c0 2.86-2.64 5.016-6.164 5.016H7.199v-15zm2.085 1.952v5.537h.07c.233-.432.858-.796 2.249-1.226 2.039-.659 3.037-1.52 3.037-2.655 0-.998-.766-1.656-1.924-1.656H9.285zm4.87 5.266c-.766.385-1.67.748-2.76 1.11-1.229.387-2.11 1.386-2.11 2.407v2.315h2.365c2.387 0 4.149-1.34 4.149-3.155 0-1.067-.625-2.087-1.645-2.677z"/>
<!-- #0B996E disc with a white letterform per brevo.com's favicon.svg. Brevo publishes no per-theme variant of the app mark; the reversed "Mint" #F9FFF6 asset is the wordmark only. -->
<svg {width} {height} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M32 16C32 24.8366 24.8366 32 16 32C7.16344 32 0 24.8366 0 16C0 7.16344 7.16344 0 16 0C24.8366 0 32 7.16344 32 16Z"
fill="#0B996E"
/>
<path
d="M21.0024 14.5395C21.9917 13.5708 22.4552 12.4513 22.4552 11.0897C22.4552 8.27639 20.3848 6.4 17.2642 6.4H9.6V26.4H15.7803C20.478 26.4 24 23.5258 24 19.7135C24 17.6254 22.9188 15.7502 21.0024 14.5395ZM12.3813 9.00158H16.9547C18.4995 9.00158 19.5198 9.87892 19.5198 11.2101C19.5198 12.7227 18.1913 13.8726 15.4721 14.7499C13.6179 15.3242 12.784 15.8086 12.4746 16.3842L12.3813 16.385V9.00158ZM15.533 23.7984H12.3813V20.7125C12.3813 19.3509 13.5558 18.0197 15.1937 17.5049C16.6466 17.0206 17.8508 16.5363 18.8711 16.0228C20.2307 16.8101 21.0646 18.1705 21.0646 19.593C21.0646 22.0133 18.7157 23.7984 15.533 23.7984Z"
fill="white"
/>
</svg>
@@ -1,12 +1,22 @@
<script lang="ts">
interface Props {
height?: string;
width?: string;
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props();
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg {width} {height} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M18.69 2.319a3.868 3.868 0 0 0-3.108 1.547l-.759 1.007a1.658 1.658 0 0 1-1.313.656H0V21.68h5.296a3.87 3.87 0 0 0 3.108-1.547l.759-1.006a1.656 1.656 0 0 1 1.313-.657H24V2.319h-5.31Zm1.108 11.949h-5.66a3.87 3.87 0 0 0-3.108 1.547l-.759 1.007a1.658 1.658 0 0 1-1.313.656H4.202V9.731h5.661a3.868 3.868 0 0 0 3.107-1.547l.759-1.006a1.658 1.658 0 0 1 1.313-.657h4.771l-.015 7.747Z"/>
<!-- #15191E / #FFFFFF per brex.com. -->
<svg
class="text-[#15191E] dark:text-[#FFFFFF]"
{width}
{height}
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18.69 2.319a3.868 3.868 0 0 0-3.108 1.547l-.759 1.007a1.658 1.658 0 0 1-1.313.656H0V21.68h5.296a3.87 3.87 0 0 0 3.108-1.547l.759-1.006a1.656 1.656 0 0 1 1.313-.657H24V2.319h-5.31Zm1.108 11.949h-5.66a3.87 3.87 0 0 0-3.108 1.547l-.759 1.007a1.658 1.658 0 0 1-1.313.656H4.202V9.731h5.661a3.868 3.868 0 0 0 3.107-1.547l.759-1.006a1.658 1.658 0 0 1 1.313-.657h4.771l-.015 7.747Z"
/>
</svg>
@@ -1,12 +1,22 @@
<script lang="ts">
interface Props {
height?: string;
width?: string;
height?: string
width?: string
}
let { height = '24px', width = '24px' }: Props = $props();
let { height = '24px', width = '24px' }: Props = $props()
</script>
<svg {width} {height} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 8h16M4 6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2zm4-2v4"/>
<!-- #000000 / #FFFFFF per browserless.io/favicon.svg. Its own prefers-color-scheme block sets black on light, white on dark. -->
<svg
class="text-[#000000] dark:text-[#FFFFFF]"
{width}
{height}
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill="currentColor"
d="m243.9 0c0.3 0.1 26.4 15 115.1 66.5v411.8c0 391.6 0.1 411.7 1.8 411.2 0.9-0.3 69.7-34 304.1-149l0.1-61.8c0-48.9-0.3-61.6-1.3-61.3-0.6 0.2-43.6 20-95.5 44-51.8 24-94.4 43.5-94.7 43.3-0.2-0.1-0.3-128.3 0-569.6l115.5 68 1.1 256.4 191.4 111.4 0.5 243.6-213.1 104.5c-117.2 57.5-213.9 104.6-214.8 104.8-0.9 0.2-26.5-16.3-112.2-73.3l0.1-296.5c0-163.1 0.3-376.9 0.7-475.2 0.3-98.4 0.9-178.8 1.2-178.8z"
/>
</svg>

Some files were not shown because too many files have changed in this diff Show More