Compare commits
25 Commits
claude/ama
...
claude/pha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f359da0bf9 | ||
|
|
311551da04 | ||
|
|
5fa5779b6b | ||
|
|
04b7513e57 | ||
|
|
848abc757a | ||
|
|
5112d0e516 | ||
|
|
9eee248877 | ||
|
|
84a95f0a3b | ||
|
|
af6d5bb0ea | ||
|
|
516d1d4eb6 | ||
|
|
728d78001a | ||
|
|
408db28330 | ||
|
|
d239ebec05 | ||
|
|
ef6b469df1 | ||
|
|
04f2109fc7 | ||
|
|
492de63c77 | ||
|
|
5834e46045 | ||
|
|
e085dc11a9 | ||
|
|
70e376f041 | ||
|
|
57e2343032 | ||
|
|
f28f9b7d55 | ||
|
|
20a900c505 | ||
|
|
e71c16050d | ||
|
|
8cfbca51b2 | ||
|
|
86723f0f6f |
32
.github/workflows/publish-server-image.yml
vendored
32
.github/workflows/publish-server-image.yml
vendored
@ -161,19 +161,16 @@ jobs:
|
|||||||
provenance: mode=max
|
provenance: mode=max
|
||||||
sbom: true
|
sbom: true
|
||||||
- name: Record the architecture digest
|
- name: Record the architecture digest
|
||||||
id: digest
|
|
||||||
env:
|
env:
|
||||||
DIGEST: ${{ steps.build.outputs.digest }}
|
DIGEST: ${{ steps.build.outputs.digest }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
digest_dir="${RUNNER_TEMP}/digest"
|
mkdir -p /tmp/digests
|
||||||
mkdir -p "$digest_dir"
|
touch "/tmp/digests/${DIGEST#sha256:}"
|
||||||
touch "${digest_dir}/${DIGEST#sha256:}"
|
|
||||||
echo "dir=${digest_dir}" >> "$GITHUB_OUTPUT"
|
|
||||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
with:
|
with:
|
||||||
name: digest-${{ matrix.name }}-${{ matrix.arch }}
|
name: digest-${{ matrix.name }}-${{ matrix.arch }}
|
||||||
path: ${{ steps.digest.outputs.dir }}/*
|
path: /tmp/digests/*
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
@ -196,13 +193,10 @@ jobs:
|
|||||||
- name: updater
|
- name: updater
|
||||||
- name: computer
|
- name: computer
|
||||||
steps:
|
steps:
|
||||||
- name: Prepare an isolated digest directory
|
|
||||||
id: digest_dir
|
|
||||||
run: echo "path=${RUNNER_TEMP}/digests" >> "$GITHUB_OUTPUT"
|
|
||||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
with:
|
with:
|
||||||
pattern: digest-${{ matrix.name }}-*
|
pattern: digest-${{ matrix.name }}-*
|
||||||
path: ${{ steps.digest_dir.outputs.path }}
|
path: /tmp/digests
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||||
- name: Log in to GHCR
|
- name: Log in to GHCR
|
||||||
@ -229,7 +223,7 @@ jobs:
|
|||||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-') }}
|
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, '-') }}
|
||||||
- name: Assemble and verify the manifest
|
- name: Assemble and verify the manifest
|
||||||
id: manifest
|
id: manifest
|
||||||
working-directory: ${{ steps.digest_dir.outputs.path }}
|
working-directory: /tmp/digests
|
||||||
env:
|
env:
|
||||||
IMAGE: ghcr.io/${{ steps.ns.outputs.repo }}/${{ matrix.name }}
|
IMAGE: ghcr.io/${{ steps.ns.outputs.repo }}/${{ matrix.name }}
|
||||||
TAGS: ${{ steps.meta.outputs.tags }}
|
TAGS: ${{ steps.meta.outputs.tags }}
|
||||||
@ -256,23 +250,7 @@ jobs:
|
|||||||
grep -Fqx "$want" <<< "$platforms" || { echo "$first_tag is missing $want" >&2; exit 1; }
|
grep -Fqx "$want" <<< "$platforms" || { echo "$first_tag is missing $want" >&2; exit 1; }
|
||||||
done
|
done
|
||||||
echo "digest=$(docker buildx imagetools inspect "$first_tag" --format '{{.Manifest.Digest}}')" >> "$GITHUB_OUTPUT"
|
echo "digest=$(docker buildx imagetools inspect "$first_tag" --format '{{.Manifest.Digest}}')" >> "$GITHUB_OUTPUT"
|
||||||
# Owner type is not exposed on the github context; resolve it from the repository metadata API.
|
|
||||||
- name: Resolve registry attestation eligibility
|
|
||||||
id: attestation
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
owner_type="$(gh api "repos/${GITHUB_REPOSITORY}" --jq -r '.owner.type')"
|
|
||||||
private="$(gh api "repos/${GITHUB_REPOSITORY}" --jq -r '.private')"
|
|
||||||
if [[ "$owner_type" == "User" && "$private" == "true" ]]; then
|
|
||||||
echo "enabled=false" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Skipping registry attestation: unavailable for private repositories on personal accounts."
|
|
||||||
else
|
|
||||||
echo "enabled=true" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
- name: Attest the published image
|
- name: Attest the published image
|
||||||
if: steps.attestation.outputs.enabled == 'true'
|
|
||||||
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
|
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
|
||||||
with:
|
with:
|
||||||
subject-name: ghcr.io/${{ steps.ns.outputs.repo }}/${{ matrix.name }}
|
subject-name: ghcr.io/${{ steps.ns.outputs.repo }}/${{ matrix.name }}
|
||||||
|
|||||||
@ -10,7 +10,6 @@
|
|||||||
- Keep code simple: reuse existing primitives and one source of truth, remove duplication and unused flexibility, and avoid speculative abstractions. Add an interface when it protects a real external or platform boundary, not for its own sake.
|
- Keep code simple: reuse existing primitives and one source of truth, remove duplication and unused flexibility, and avoid speculative abstractions. Add an interface when it protects a real external or platform boundary, not for its own sake.
|
||||||
- Treat auth, secret handling, sandbox boundaries, host commands, and integrations as security-sensitive. Keep tests deterministic and offline by default.
|
- Treat auth, secret handling, sandbox boundaries, host commands, and integrations as security-sensitive. Keep tests deterministic and offline by default.
|
||||||
- The desktop Playwright suite (`pnpm --filter @rakazo/desktop test:e2e`) opens real Electron windows and steals focus on macOS. Do not run it on a maintainer's machine as routine verification; run the unit tests locally and let CI's virtual-display job run the e2e on push.
|
- The desktop Playwright suite (`pnpm --filter @rakazo/desktop test:e2e`) opens real Electron windows and steals focus on macOS. Do not run it on a maintainer's machine as routine verification; run the unit tests locally and let CI's virtual-display job run the e2e on push.
|
||||||
- Work autonomously. Make routine calls yourself, such as opening or reopening a pull request, linking CI screenshots, or fixing gaps you found that belong to the task, then verify and report the outcome. Ask the maintainer only before irreversible actions or for product decisions without a sensible default.
|
|
||||||
- After creating a pull request, stay with it until CI and automated review bots have finished; passing checks alone do not mean the review is complete. Follow `.agents/skills/pr-watch/SKILL.md`: run its `pr-digest --watch` helper as a single backgrounded call that blocks until the checks on the current head commit are terminal, then act on the verdict it prints. Do not poll in the foreground on a timer, which mostly buys repeated snapshots of an unchanged PR and dies at the tool timeout. Address every actionable issue, push the fixes, and repeat until no actionable feedback remains. Do not merge while review bots are still pending or review issues remain unresolved.
|
- After creating a pull request, stay with it until CI and automated review bots have finished; passing checks alone do not mean the review is complete. Follow `.agents/skills/pr-watch/SKILL.md`: run its `pr-digest --watch` helper as a single backgrounded call that blocks until the checks on the current head commit are terminal, then act on the verdict it prints. Do not poll in the foreground on a timer, which mostly buys repeated snapshots of an unchanged PR and dies at the tool timeout. Address every actionable issue, push the fixes, and repeat until no actionable feedback remains. Do not merge while review bots are still pending or review issues remain unresolved.
|
||||||
- PR descriptions say why the change exists, what changed, and how it was tested. Write Why as the product reason (what was wrong and what this changes); never "Elie wants" or a third-person briefing of a named person; the maintainer is the author.
|
- PR descriptions say why the change exists, what changed, and how it was tested. Write Why as the product reason (what was wrong and what this changes); never "Elie wants" or a third-person briefing of a named person; the maintainer is the author.
|
||||||
- Commit messages, PR descriptions, issues, and review replies are public too. Describe test results in words instead of pasting tool output, and never include anything that identifies a person, machine, or account: local paths, usernames, hostnames, signing identities, legal entity names, Apple Team IDs, API key IDs, issuer or tenant IDs, account emails, or the accounts, machines, and files that secret values came from. Naming the secrets a workflow reads is fine; use placeholders when a value must be shown.
|
- Commit messages, PR descriptions, issues, and review replies are public too. Describe test results in words instead of pasting tool output, and never include anything that identifies a person, machine, or account: local paths, usernames, hostnames, signing identities, legal entity names, Apple Team IDs, API key IDs, issuer or tenant IDs, account emails, or the accounts, machines, and files that secret values came from. Naming the secrets a workflow reads is fine; use placeholders when a value must be shown.
|
||||||
|
|||||||
@ -1,264 +0,0 @@
|
|||||||
import { expect, type Locator, type Page, test } from "@playwright/test";
|
|
||||||
import {
|
|
||||||
captureScreenshot,
|
|
||||||
completeOnboarding,
|
|
||||||
createNamedBot,
|
|
||||||
openNewBot,
|
|
||||||
rpc,
|
|
||||||
signup,
|
|
||||||
waitForChatComposer,
|
|
||||||
} from "./helpers";
|
|
||||||
|
|
||||||
// WCAG AA for body text.
|
|
||||||
const MIN_CONTRAST = 4.5;
|
|
||||||
|
|
||||||
test.use({ viewport: { width: 1280, height: 1000 } });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Texts in `locator` below WCAG AA against the surface they are painted on. With `inside`, every
|
|
||||||
* visible text and typed value within it counts, except in disabled controls, which WCAG exempts,
|
|
||||||
* and in elements matching `skip`.
|
|
||||||
*/
|
|
||||||
function unreadableTexts(locator: Locator, inside: boolean, skip: string | null) {
|
|
||||||
return locator.evaluate(
|
|
||||||
(root, { inside, skip, min }) => {
|
|
||||||
const context = document
|
|
||||||
.createElement("canvas")
|
|
||||||
.getContext("2d", { willReadFrequently: true });
|
|
||||||
if (!context) throw new Error("No 2D canvas context");
|
|
||||||
// The canvas normalizes every CSS color syntax (oklab, color-mix, ...) to sRGB.
|
|
||||||
const rgba = (color: string) => {
|
|
||||||
context.clearRect(0, 0, 1, 1);
|
|
||||||
context.fillStyle = color;
|
|
||||||
context.fillRect(0, 0, 1, 1);
|
|
||||||
const [r = 0, g = 0, b = 0, a = 0] = context.getImageData(0, 0, 1, 1).data;
|
|
||||||
return [r / 255, g / 255, b / 255, a / 255] as const;
|
|
||||||
};
|
|
||||||
// One pixel of `color`, painted inside `from` and composited through every ancestor's
|
|
||||||
// background color and opacity onto the white canvas.
|
|
||||||
const paint = (color: readonly [number, number, number, number], from: Element) => {
|
|
||||||
let [r, g, b, a] = color;
|
|
||||||
for (let node: Element | null = from; node; node = node.parentElement) {
|
|
||||||
const style = getComputedStyle(node);
|
|
||||||
const [br, bg, bb, ba] = rgba(style.backgroundColor);
|
|
||||||
const alpha = a + ba * (1 - a);
|
|
||||||
if (alpha > 0) {
|
|
||||||
r = (r * a + br * ba * (1 - a)) / alpha;
|
|
||||||
g = (g * a + bg * ba * (1 - a)) / alpha;
|
|
||||||
b = (b * a + bb * ba * (1 - a)) / alpha;
|
|
||||||
}
|
|
||||||
a = alpha * Number(style.opacity);
|
|
||||||
}
|
|
||||||
return [r * a + 1 - a, g * a + 1 - a, b * a + 1 - a] as const;
|
|
||||||
};
|
|
||||||
const luminance = (rgb: readonly [number, number, number]) => {
|
|
||||||
const [r = 0, g = 0, b = 0] = rgb.map((channel) =>
|
|
||||||
channel <= 0.04045 ? channel / 12.92 : ((channel + 0.055) / 1.055) ** 2.4,
|
|
||||||
);
|
|
||||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
||||||
};
|
|
||||||
const contrast = (element: Element) => {
|
|
||||||
const text = luminance(paint(rgba(getComputedStyle(element).color), element));
|
|
||||||
const surface = luminance(paint([0, 0, 0, 0], element));
|
|
||||||
return (Math.max(text, surface) + 0.05) / (Math.min(text, surface) + 0.05);
|
|
||||||
};
|
|
||||||
// What the element itself shows: a typed value, or its own text nodes.
|
|
||||||
const ownText = (element: Element) => {
|
|
||||||
if (element instanceof HTMLTextAreaElement) return element.value;
|
|
||||||
if (element instanceof HTMLInputElement) {
|
|
||||||
return /^(text|search|url|email|tel|number|password)$/.test(element.type)
|
|
||||||
? element.value
|
|
||||||
: "";
|
|
||||||
}
|
|
||||||
return Array.from(element.childNodes, (node) =>
|
|
||||||
node.nodeType === Node.TEXT_NODE ? node.textContent : "",
|
|
||||||
)
|
|
||||||
.join("")
|
|
||||||
.trim();
|
|
||||||
};
|
|
||||||
const shown = (element: Element) => {
|
|
||||||
const { width, height } = element.getBoundingClientRect();
|
|
||||||
return width > 1 && height > 1 && element.checkVisibility();
|
|
||||||
};
|
|
||||||
const elements = inside
|
|
||||||
? [root, ...root.querySelectorAll("*")].filter(
|
|
||||||
(element) =>
|
|
||||||
ownText(element) &&
|
|
||||||
shown(element) &&
|
|
||||||
!element.closest(":disabled") &&
|
|
||||||
!(skip && element.closest(skip)),
|
|
||||||
)
|
|
||||||
: [root];
|
|
||||||
return elements.flatMap((element) => {
|
|
||||||
const ratio = contrast(element);
|
|
||||||
const text = ownText(element) || element.textContent?.trim() || "";
|
|
||||||
return ratio < min ? [`${ratio.toFixed(2)}:1 ${text.slice(0, 48)}`] : [];
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ inside, skip, min: MIN_CONTRAST },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function expectReadable(
|
|
||||||
locator: Locator,
|
|
||||||
what: string,
|
|
||||||
{ inside = false, skip = null }: { inside?: boolean; skip?: string | null } = {},
|
|
||||||
) {
|
|
||||||
await expect(locator).toBeVisible();
|
|
||||||
await expect
|
|
||||||
.poll(() => unreadableTexts(locator, inside, skip), { message: `${what} is readable` })
|
|
||||||
.toEqual([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function inEachTheme(page: Page, check: (theme: "light" | "dark") => Promise<void>) {
|
|
||||||
for (const theme of ["light", "dark"] as const) {
|
|
||||||
await page.emulateMedia({ colorScheme: theme });
|
|
||||||
await expect(page.locator("html")).toHaveAttribute("data-theme", theme);
|
|
||||||
await check(theme);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function runStatus(page: Page, groupId: string) {
|
|
||||||
return rpc<{ run?: { status: string } | null }>(page, "threads/get", { groupId }).then(
|
|
||||||
(snapshot) => snapshot.run?.status ?? "idle",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function send(page: Page, groupId: string, text: string) {
|
|
||||||
await expect
|
|
||||||
.poll(() => runStatus(page, groupId), { timeout: 60_000 })
|
|
||||||
.not.toMatch(/^(queued|running)$/);
|
|
||||||
const composer = page.getByRole("combobox", { name: "Message" });
|
|
||||||
await composer.fill(text);
|
|
||||||
await composer.press("Enter");
|
|
||||||
}
|
|
||||||
|
|
||||||
test("conversation text stays readable in light and dark themes", async ({ page }, testInfo) => {
|
|
||||||
const stamp = Date.now();
|
|
||||||
await page.emulateMedia({ colorScheme: "light" });
|
|
||||||
await signup(page, `chat-contrast-${stamp}@rakazo.test`, "password12", "Contrast QA");
|
|
||||||
await completeOnboarding(page, undefined, { mainConversation: true });
|
|
||||||
const groupId = new URL(page.url()).pathname.split("/").at(-1)!;
|
|
||||||
await rpc(page, "bots/create", {
|
|
||||||
name: "Researcher",
|
|
||||||
title: "",
|
|
||||||
description: "",
|
|
||||||
instructions: "",
|
|
||||||
notifyOnFinish: true,
|
|
||||||
});
|
|
||||||
await page.reload();
|
|
||||||
await waitForChatComposer(page);
|
|
||||||
await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
|
|
||||||
const transcript = page.getByTestId("transcript");
|
|
||||||
|
|
||||||
// The scripted bot echoes the request, so its reply renders this markdown.
|
|
||||||
await send(
|
|
||||||
page,
|
|
||||||
groupId,
|
|
||||||
"Summarize the **owl facts** in `notes/owls.md`\n\n## Owl notes\n\n> Owls turn their heads\n\n```\nowls = 3\n```",
|
|
||||||
);
|
|
||||||
const firstMessage = transcript
|
|
||||||
.getByTestId("message-user-bubble")
|
|
||||||
.filter({ hasText: "Summarize the **owl facts**" });
|
|
||||||
const markdown = transcript
|
|
||||||
.locator(".rk-chat-markdown")
|
|
||||||
.filter({ hasText: "done. i handled: Summarize the owl facts" });
|
|
||||||
const reply = markdown.locator("p").filter({ hasText: "done. i handled" });
|
|
||||||
await expect(reply).toBeVisible({ timeout: 60_000 });
|
|
||||||
|
|
||||||
await send(page, groupId, "delegate to the bot named Researcher: collect three owl facts");
|
|
||||||
const task = transcript.getByTestId("delegated-task-card");
|
|
||||||
await expect(task).toHaveAttribute("data-status", "done", { timeout: 60_000 });
|
|
||||||
await expect(
|
|
||||||
transcript.getByText("closing the task: collect three owl facts.").first(),
|
|
||||||
).toBeVisible({ timeout: 60_000 });
|
|
||||||
|
|
||||||
await send(page, groupId, "ask me which city to use");
|
|
||||||
await expect.poll(() => runStatus(page, groupId), { timeout: 60_000 }).toBe("waiting_input");
|
|
||||||
const question = transcript
|
|
||||||
.locator(".rk-chat-markdown p")
|
|
||||||
.filter({ hasText: /^Which city should I use\?$/ });
|
|
||||||
// threads/get can observe waiting_input before the realtime feed paints the ask card.
|
|
||||||
if ((await question.count()) === 0) {
|
|
||||||
await page.reload({ waitUntil: "domcontentloaded" });
|
|
||||||
await waitForChatComposer(page);
|
|
||||||
}
|
|
||||||
await expect(question).toBeVisible({ timeout: 15_000 });
|
|
||||||
const lastMessage = transcript
|
|
||||||
.getByTestId("message-user-bubble")
|
|
||||||
.filter({ hasText: "ask me which city to use" });
|
|
||||||
await task.locator("summary").click();
|
|
||||||
const brief = task.locator(".rk-chat-markdown p");
|
|
||||||
await expect(brief).toHaveText("collect three owl facts");
|
|
||||||
|
|
||||||
await inEachTheme(page, async (theme) => {
|
|
||||||
await firstMessage.scrollIntoViewIfNeeded();
|
|
||||||
await captureScreenshot(page, testInfo, `chat-contrast-${theme}-reply`);
|
|
||||||
await expectReadable(firstMessage, `${theme}: the user's own message`);
|
|
||||||
await expectReadable(reply, `${theme}: a bot reply`);
|
|
||||||
await expectReadable(reply.locator("strong"), `${theme}: bold text in a bot reply`);
|
|
||||||
await expectReadable(reply.locator("code"), `${theme}: inline code in a bot reply`);
|
|
||||||
await expectReadable(markdown.locator("h2"), `${theme}: a heading in a bot reply`);
|
|
||||||
await expectReadable(markdown.locator("blockquote p"), `${theme}: a quote in a bot reply`);
|
|
||||||
await expectReadable(markdown.locator("pre code"), `${theme}: a code block in a bot reply`);
|
|
||||||
|
|
||||||
await brief.scrollIntoViewIfNeeded();
|
|
||||||
await captureScreenshot(page, testInfo, `chat-contrast-${theme}-task`);
|
|
||||||
await expectReadable(brief, `${theme}: the delegated task brief`);
|
|
||||||
|
|
||||||
await question.scrollIntoViewIfNeeded();
|
|
||||||
await captureScreenshot(page, testInfo, `chat-contrast-${theme}-ask`);
|
|
||||||
await expectReadable(question, `${theme}: the ask card question`);
|
|
||||||
await expectReadable(lastMessage, `${theme}: the latest user message`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("shell surfaces stay readable in light and dark themes", async ({ page }, testInfo) => {
|
|
||||||
await page.emulateMedia({ colorScheme: "light" });
|
|
||||||
await signup(page, `shell-contrast-${Date.now()}@rakazo.test`, "password12", "Contrast QA");
|
|
||||||
await completeOnboarding(page);
|
|
||||||
await createNamedBot(page, "Researcher");
|
|
||||||
const composer = page.getByRole("combobox", { name: "Message" });
|
|
||||||
await composer.fill("Draft a note about owls");
|
|
||||||
const sidebar = page.getByTestId("bots-sidebar");
|
|
||||||
const sidePanel = page.getByTestId("side-panel");
|
|
||||||
await expect(sidebar.locator("[data-main-conversation]")).toBeVisible();
|
|
||||||
await expect(sidebar.getByRole("button", { name: "Researcher, 1:1 conversation" })).toBeVisible();
|
|
||||||
await expect(page.getByTestId("trading-mode-status")).toBeVisible();
|
|
||||||
|
|
||||||
await inEachTheme(page, async (theme) => {
|
|
||||||
await captureScreenshot(page, testInfo, `shell-contrast-${theme}`);
|
|
||||||
await expectReadable(composer, `${theme}: the composer draft`);
|
|
||||||
await expectReadable(page.getByTestId("composer-bar"), `${theme}: the composer`, {
|
|
||||||
inside: true,
|
|
||||||
});
|
|
||||||
// The header carries the logo, which WCAG exempts; its create button is checked on its own.
|
|
||||||
await expectReadable(sidebar, `${theme}: the sidebar`, { inside: true, skip: ".app-drag" });
|
|
||||||
await expectReadable(page.getByTestId("create-menu-trigger"), `${theme}: the create button`);
|
|
||||||
});
|
|
||||||
|
|
||||||
await openNewBot(page);
|
|
||||||
await page.getByTestId("create-bot-form").locator("label:has-text('Name') input").fill("Owl");
|
|
||||||
await inEachTheme(page, async (theme) => {
|
|
||||||
await captureScreenshot(page, testInfo, `shell-contrast-${theme}-new-bot`);
|
|
||||||
await expectReadable(sidePanel, `${theme}: the new bot form`, { inside: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Cancel new bot" }).click();
|
|
||||||
await page.getByTestId("bot-settings-trigger").click();
|
|
||||||
const advanced = page.getByTestId("bot-settings-advanced");
|
|
||||||
await advanced.locator("summary").click();
|
|
||||||
await expect(advanced.getByText("Open work")).toBeVisible();
|
|
||||||
await inEachTheme(page, async (theme) => {
|
|
||||||
await captureScreenshot(page, testInfo, `shell-contrast-${theme}-settings`);
|
|
||||||
await expectReadable(sidePanel, `${theme}: the bot settings form`, { inside: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
await sidebar.getByRole("button", { name: "Heute", exact: true }).click();
|
|
||||||
const heute = page.getByTestId("heute-brief");
|
|
||||||
await expect(heute.getByTestId("heute-mission-control-link")).toBeVisible();
|
|
||||||
await inEachTheme(page, async (theme) => {
|
|
||||||
await captureScreenshot(page, testInfo, `shell-contrast-${theme}-heute`);
|
|
||||||
await expectReadable(heute, `${theme}: the Heute brief`, { inside: true });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -10,16 +10,16 @@ const TRUST_ITEMS = [
|
|||||||
export const DataTrustPanel = memo(function DataTrustPanel() {
|
export const DataTrustPanel = memo(function DataTrustPanel() {
|
||||||
return (
|
return (
|
||||||
<div className="hidden xl:flex flex-col items-start gap-1.5 rounded-xl border border-border/40 bg-muted/30 px-3 py-2.5">
|
<div className="hidden xl:flex flex-col items-start gap-1.5 rounded-xl border border-border/40 bg-muted/30 px-3 py-2.5">
|
||||||
<div className="flex items-center gap-1.5 text-xs font-medium text-sidebar-foreground">
|
<div className="flex items-center gap-1.5 text-xs font-medium text-emerald-600 dark:text-emerald-400">
|
||||||
<CheckCircle2 className="size-3.5 text-success" />
|
<CheckCircle2 className="size-3.5" />
|
||||||
<span>Datenschutz aktiv</span>
|
<span>Datenschutz aktiv</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{TRUST_ITEMS.map(({ icon: Icon, label, desc }) => (
|
{TRUST_ITEMS.map(({ icon: Icon, label, desc }) => (
|
||||||
<div key={label} className="flex items-center gap-1.5 rk-sidebar-muted">
|
<div key={label} className="flex items-center gap-1.5">
|
||||||
<Icon className="size-3" />
|
<Icon className="size-3 text-muted-foreground/60" />
|
||||||
<span className="text-[11px] leading-tight">{label}</span>
|
<span className="text-[11px] leading-tight text-muted-foreground">{label}</span>
|
||||||
<span className="text-[10px] leading-tight">· {desc}</span>
|
<span className="text-[10px] leading-tight text-muted-foreground/50">· {desc}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export function TradingModeIndicator({
|
|||||||
<TradingModeLabel label={presentation.label} />
|
<TradingModeLabel label={presentation.label} />
|
||||||
</span>
|
</span>
|
||||||
{presentation.subtitle ? (
|
{presentation.subtitle ? (
|
||||||
<span className="block truncate text-ui-body-sm rk-sidebar-muted">
|
<span className="block truncate text-ui-body-sm text-muted-foreground/70">
|
||||||
<TradingModeSubtitleText subtitle={presentation.subtitle} />
|
<TradingModeSubtitleText subtitle={presentation.subtitle} />
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@ -175,7 +175,7 @@ export function OoStage({
|
|||||||
{(hasMessages || phase !== "idle") && (
|
{(hasMessages || phase !== "idle") && (
|
||||||
<div className={`flex w-full justify-center ${hasMessages ? "mb-1" : "mb-3"}`}>
|
<div className={`flex w-full justify-center ${hasMessages ? "mb-1" : "mb-3"}`}>
|
||||||
<div
|
<div
|
||||||
className="inline-flex items-center gap-2 rounded-full border border-border bg-foreground/5 px-2.5 py-0.5 text-ui-label font-semibold uppercase tracking-wider text-muted-foreground backdrop-blur-sm"
|
className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-0.5 text-ui-label font-semibold uppercase tracking-wider text-muted-foreground backdrop-blur-sm"
|
||||||
style={
|
style={
|
||||||
reducedMotion
|
reducedMotion
|
||||||
? undefined
|
? undefined
|
||||||
@ -280,7 +280,7 @@ function StageEnsemble({
|
|||||||
return (
|
return (
|
||||||
<li key={member.botId} className="flex flex-col items-center gap-1" title={member.name}>
|
<li key={member.botId} className="flex flex-col items-center gap-1" title={member.name}>
|
||||||
<div
|
<div
|
||||||
className={`relative flex size-14 items-center justify-center rounded-full border border-border bg-muted/40 backdrop-blur-md ${
|
className={`relative flex size-14 items-center justify-center rounded-full border border-white/12 bg-slate-950/40 backdrop-blur-md ${
|
||||||
isWorking && !reducedMotion ? "animate-pulse" : ""
|
isWorking && !reducedMotion ? "animate-pulse" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -484,7 +484,7 @@ export function OoGroupStage({
|
|||||||
className={`relative z-10 mx-auto flex w-full max-w-3xl flex-col items-center px-4 md:px-8 ${hasMessages ? "py-2 md:py-3" : "py-5 md:py-6"}`}
|
className={`relative z-10 mx-auto flex w-full max-w-3xl flex-col items-center px-4 md:px-8 ${hasMessages ? "py-2 md:py-3" : "py-5 md:py-6"}`}
|
||||||
>
|
>
|
||||||
<div className="mb-3 w-full min-w-0 text-center">
|
<div className="mb-3 w-full min-w-0 text-center">
|
||||||
<div className="inline-flex items-center gap-2 rounded-full border border-border bg-foreground/5 px-2.5 py-0.5 text-ui-label font-semibold uppercase tracking-wider text-muted-foreground backdrop-blur-sm">
|
<div className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.05] px-2.5 py-0.5 text-ui-label font-semibold uppercase tracking-wider text-muted-foreground backdrop-blur-sm">
|
||||||
<span
|
<span
|
||||||
className={`size-1.5 rounded-full ${
|
className={`size-1.5 rounded-full ${
|
||||||
running ? "bg-violet-400" : "bg-emerald-400"
|
running ? "bg-violet-400" : "bg-emerald-400"
|
||||||
|
|||||||
@ -55,7 +55,7 @@ export function StageHero({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`relative flex ${orbSize} items-center justify-center rounded-full border border-border bg-muted/45 shadow-[0_16px_48px_rgba(0,0,0,0.45),inset_0_1px_0_rgba(255,255,255,0.12)] backdrop-blur-xl`}
|
className={`relative flex ${orbSize} items-center justify-center rounded-full border border-white/14 bg-slate-950/45 shadow-[0_16px_48px_rgba(0,0,0,0.45),inset_0_1px_0_rgba(255,255,255,0.12)] backdrop-blur-xl`}
|
||||||
style={
|
style={
|
||||||
reducedMotion
|
reducedMotion
|
||||||
? undefined
|
? undefined
|
||||||
|
|||||||
@ -35,7 +35,7 @@ export function TextStageCanvas({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div aria-hidden className="pointer-events-none absolute inset-0 overflow-hidden">
|
<div aria-hidden className="pointer-events-none absolute inset-0 overflow-hidden">
|
||||||
<div className="absolute inset-0 bg-radial-[ellipse_100%_70%_at_50%_0%] from-card/55 to-transparent to-68%" />
|
<div className="absolute inset-0 bg-[radial-gradient(ellipse_100%_70%_at_50%_0%,rgba(12,16,24,0.55)_0%,transparent_68%)]" />
|
||||||
<div
|
<div
|
||||||
className={`absolute left-1/2 top-0 h-[min(48vh,320px)] w-[min(72vw,480px)] -translate-x-1/2 rounded-full blur-[72px] ${reducedMotion ? "" : "voice-bloom-emerald"}`}
|
className={`absolute left-1/2 top-0 h-[min(48vh,320px)] w-[min(72vw,480px)] -translate-x-1/2 rounded-full blur-[72px] ${reducedMotion ? "" : "voice-bloom-emerald"}`}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@ -28,15 +28,15 @@ export function VoiceAwarenessStrip({
|
|||||||
<div
|
<div
|
||||||
role="status"
|
role="status"
|
||||||
data-testid="voice-awareness"
|
data-testid="voice-awareness"
|
||||||
className="mx-auto flex max-w-2xl items-start gap-3 rounded-[20px] border border-border bg-muted/40 px-4 py-3 backdrop-blur-md"
|
className="mx-auto flex max-w-2xl items-start gap-3 rounded-[20px] border border-white/10 bg-white/[0.05] px-4 py-3 backdrop-blur-md"
|
||||||
>
|
>
|
||||||
<BotAvatar color={botColor} identity={botId} size={32} className="shrink-0" />
|
<BotAvatar color={botColor} identity={botId} size={32} className="shrink-0" />
|
||||||
<div className="min-w-0 flex-1 text-start">
|
<div className="min-w-0 flex-1 text-start">
|
||||||
<div className="flex items-center gap-2 text-ui-label font-semibold uppercase tracking-wide text-muted-foreground">
|
<div className="flex items-center gap-2 text-ui-label font-semibold uppercase tracking-wide text-zinc-300">
|
||||||
<Icon strokeWidth={1.8} className="size-icon-sm shrink-0 opacity-80" />
|
<Icon strokeWidth={1.8} className="size-icon-sm shrink-0 opacity-80" />
|
||||||
<span>{awarenessKindLabel(event.kind)}</span>
|
<span>{awarenessKindLabel(event.kind)}</span>
|
||||||
</div>
|
</div>
|
||||||
<p dir="auto" className="mt-1 text-ui-body leading-relaxed text-foreground">
|
<p dir="auto" className="mt-1 text-ui-body leading-relaxed text-zinc-100">
|
||||||
{label}
|
{label}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -178,7 +178,7 @@ export function HeuteBrief({
|
|||||||
href={MISSION_CONTROL_URL}
|
href={MISSION_CONTROL_URL}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-ui-label text-muted-foreground transition-colors hover:text-foreground"
|
className="inline-flex items-center gap-1.5 rounded-lg px-2 py-1 text-ui-label text-muted-foreground/70 transition-colors hover:text-muted-foreground"
|
||||||
data-testid="heute-mission-control-link"
|
data-testid="heute-mission-control-link"
|
||||||
>
|
>
|
||||||
<Trans>Mission Control</Trans>
|
<Trans>Mission Control</Trans>
|
||||||
@ -235,7 +235,7 @@ function EmptyDay({ onStartChat, hasBots }: { onStartChat: () => void; hasBots:
|
|||||||
|
|
||||||
function ActionableChip({ label }: { label: string }) {
|
function ActionableChip({ label }: { label: string }) {
|
||||||
return (
|
return (
|
||||||
<span className="shrink-0 rounded-full border border-warning/30 bg-warning/10 px-2 py-0.5 text-ui-label font-medium text-foreground dark:text-warning">
|
<span className="shrink-0 rounded-full border border-warning/30 bg-warning/10 px-2 py-0.5 text-ui-label font-medium text-warning">
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -106,7 +106,7 @@ export function ScratchpadSection({ botId }: { botId: string }) {
|
|||||||
<Trans>Open work</Trans>
|
<Trans>Open work</Trans>
|
||||||
</div>
|
</div>
|
||||||
{items.length === 0 ? (
|
{items.length === 0 ? (
|
||||||
<div className="py-1 text-ui-body text-muted-foreground">
|
<div className="py-1 text-ui-body text-muted-foreground/80">
|
||||||
<Trans>None yet</Trans>
|
<Trans>None yet</Trans>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -3354,7 +3354,7 @@ export function ShellPage() {
|
|||||||
<div
|
<div
|
||||||
data-testid="shell-root"
|
data-testid="shell-root"
|
||||||
data-ready={shellReady}
|
data-ready={shellReady}
|
||||||
className="relative flex h-full min-w-0 overflow-hidden bg-background text-foreground/90 p-0 md:p-3 gap-0 md:gap-3"
|
className="relative flex h-full min-w-0 overflow-hidden bg-black bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-zinc-900 via-black to-black text-foreground/90 p-0 md:p-3 gap-0 md:gap-3"
|
||||||
onTouchStartCapture={(event) => {
|
onTouchStartCapture={(event) => {
|
||||||
if (
|
if (
|
||||||
mobileSidebarOpen ||
|
mobileSidebarOpen ||
|
||||||
@ -3428,7 +3428,7 @@ export function ShellPage() {
|
|||||||
width: mobileSidebarOpen ? undefined : botsSidebarCollapsed ? 0 : leftSidebarWidth,
|
width: mobileSidebarOpen ? undefined : botsSidebarCollapsed ? 0 : leftSidebarWidth,
|
||||||
maxWidth: mobileSidebarOpen ? 316 : undefined,
|
maxWidth: mobileSidebarOpen ? 316 : undefined,
|
||||||
}}
|
}}
|
||||||
className={`absolute inset-y-0 start-0 z-40 flex w-[calc(100%-48px)] shrink-0 flex-col md:rounded-2xl md:border md:border-sidebar-border md:shadow-2xl bg-sidebar md:static md:z-auto md:translate-x-0 ${
|
className={`absolute inset-y-0 start-0 z-40 flex w-[calc(100%-48px)] shrink-0 flex-col md:rounded-2xl md:border md:border-white/10 md:bg-[#0A0A0C] md:shadow-2xl bg-sidebar md:static md:z-auto md:translate-x-0 ${
|
||||||
mobileSidebarOpen ? "translate-x-0" : "-translate-x-full rtl:translate-x-full"
|
mobileSidebarOpen ? "translate-x-0" : "-translate-x-full rtl:translate-x-full"
|
||||||
} ${
|
} ${
|
||||||
botsSidebarCollapsed
|
botsSidebarCollapsed
|
||||||
@ -3456,7 +3456,7 @@ export function ShellPage() {
|
|||||||
className={`app-no-drag flex size-control-sm items-center justify-center rounded-full ${
|
className={`app-no-drag flex size-control-sm items-center justify-center rounded-full ${
|
||||||
activityMode
|
activityMode
|
||||||
? "bg-primary text-primary-foreground"
|
? "bg-primary text-primary-foreground"
|
||||||
: "rk-sidebar-muted hover:text-foreground/75"
|
: "text-muted-foreground/70 hover:text-foreground/75"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Bell
|
<Bell
|
||||||
@ -3469,7 +3469,7 @@ export function ShellPage() {
|
|||||||
|
|
||||||
<Popover open={createMenuOpen} onOpenChange={setCreateMenuOpen}>
|
<Popover open={createMenuOpen} onOpenChange={setCreateMenuOpen}>
|
||||||
<PopoverTrigger
|
<PopoverTrigger
|
||||||
className="app-no-drag text-[21px] rk-sidebar-muted hover:text-foreground/75"
|
className="app-no-drag text-[21px] text-muted-foreground/70 hover:text-foreground/75"
|
||||||
title={t`Create`}
|
title={t`Create`}
|
||||||
data-testid="create-menu-trigger"
|
data-testid="create-menu-trigger"
|
||||||
>
|
>
|
||||||
@ -3565,9 +3565,13 @@ export function ShellPage() {
|
|||||||
setActiveNavPillar("heute");
|
setActiveNavPillar("heute");
|
||||||
setMobileSidebarOpen(false);
|
setMobileSidebarOpen(false);
|
||||||
}}
|
}}
|
||||||
className={rosterRowClass(activeNavPillar === "heute")}
|
className={`flex w-full items-center gap-3 rounded-2xl px-3 py-3 text-start transition-all duration-150 cursor-pointer min-h-[52px] ${
|
||||||
|
activeNavPillar === "heute"
|
||||||
|
? "bg-white/[0.09] shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_4px_16px_rgba(0,0,0,0.4)] border border-emerald-500/30"
|
||||||
|
: "hover:bg-white/[0.04]"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className="relative grid size-control-lg shrink-0 place-items-center rounded-full bg-muted text-muted-foreground">
|
<div className="relative grid size-control-lg shrink-0 place-items-center rounded-full bg-emerald-500/10 text-emerald-400">
|
||||||
<Calendar strokeWidth={2.2} className="size-icon-md" />
|
<Calendar strokeWidth={2.2} className="size-icon-md" />
|
||||||
{heuteActionCount > 0 ? (
|
{heuteActionCount > 0 ? (
|
||||||
<span
|
<span
|
||||||
@ -3592,7 +3596,7 @@ export function ShellPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label={t`Neue Unterhaltung`}
|
aria-label={t`Neue Unterhaltung`}
|
||||||
data-testid="conversation-picker-trigger"
|
data-testid="conversation-picker-trigger"
|
||||||
className="rk-sidebar-muted hover:text-foreground/90 transition-colors"
|
className="text-muted-foreground/60 hover:text-foreground/90 transition-colors"
|
||||||
>
|
>
|
||||||
<Plus strokeWidth={2.5} className="size-icon-md" />
|
<Plus strokeWidth={2.5} className="size-icon-md" />
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@ -3633,7 +3637,7 @@ export function ShellPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{conversationSidebarGroups.length === 0 && !mainConversationEntry ? (
|
{conversationSidebarGroups.length === 0 && !mainConversationEntry ? (
|
||||||
<div className="p-4 text-center text-ui-label rk-sidebar-muted">
|
<div className="p-4 text-center text-ui-label text-muted-foreground">
|
||||||
<p>{t`Noch keine Unterhaltung.`}</p>
|
<p>{t`Noch keine Unterhaltung.`}</p>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -3657,7 +3661,7 @@ export function ShellPage() {
|
|||||||
<div className="flex items-center pt-3 pb-0.5">
|
<div className="flex items-center pt-3 pb-0.5">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex min-w-0 flex-1 items-center justify-between gap-2 rounded-lg px-2.5 py-1 text-ui-label font-semibold tracking-wider uppercase text-foreground/65 dark:text-muted-foreground/80 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring"
|
className="flex min-w-0 flex-1 items-center justify-between gap-2 rounded-lg px-2.5 py-1 text-ui-label font-semibold tracking-wider uppercase text-muted-foreground/60 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (group.emptySpaceId) {
|
if (group.emptySpaceId) {
|
||||||
openSpaceChat(group.emptySpaceId, "/onboarding");
|
openSpaceChat(group.emptySpaceId, "/onboarding");
|
||||||
@ -3821,10 +3825,12 @@ export function ShellPage() {
|
|||||||
status={item.chat.status}
|
status={item.chat.status}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={`absolute -bottom-0.5 -right-0.5 size-2.5 rounded-full border-2 border-sidebar ${
|
className={`absolute -bottom-0.5 -right-0.5 size-2.5 rounded-full border-2 border-[#0b0c10] ${
|
||||||
rosterStatusDot(item.chat.status) === "idle"
|
rosterStatusDot(item.chat.status) === "needs-you"
|
||||||
? "bg-success"
|
? "bg-warning animate-pulse"
|
||||||
: "bg-warning animate-pulse"
|
: rosterStatusDot(item.chat.status) === "working"
|
||||||
|
? "bg-amber-400 animate-pulse"
|
||||||
|
: "bg-emerald-400"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -3841,11 +3847,11 @@ export function ShellPage() {
|
|||||||
{item.chat.name}
|
{item.chat.name}
|
||||||
</span>
|
</span>
|
||||||
{needsYouChipLabel(item.chat.status) ? (
|
{needsYouChipLabel(item.chat.status) ? (
|
||||||
<span className="shrink-0 rounded-full border border-warning/30 bg-warning/10 px-2 py-0.5 text-ui-label font-medium text-foreground dark:text-warning">
|
<span className="shrink-0 rounded-full border border-warning/30 bg-warning/10 px-2 py-0.5 text-ui-label font-medium text-warning">
|
||||||
{needsYouChipLabel(item.chat.status)}
|
{needsYouChipLabel(item.chat.status)}
|
||||||
</span>
|
</span>
|
||||||
) : item.chat.title ? (
|
) : item.chat.title ? (
|
||||||
<span className="min-w-0 truncate rounded-full border border-border bg-muted px-2 py-0.5 text-ui-label font-medium text-muted-foreground">
|
<span className="min-w-0 truncate rounded-full border border-emerald-500/25 bg-emerald-500/10 px-2 py-0.5 text-ui-label font-medium text-emerald-400">
|
||||||
{item.chat.title}
|
{item.chat.title}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
@ -3887,7 +3893,7 @@ export function ShellPage() {
|
|||||||
[group.key]: !prev[group.key],
|
[group.key]: !prev[group.key],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
className="mt-1 w-full flex items-center justify-center gap-1.5 rounded-lg px-2 py-1.5 text-ui-label font-medium rk-sidebar-muted hover:bg-sidebar-accent hover:text-foreground transition-colors"
|
className="mt-1 w-full flex items-center justify-center gap-1.5 rounded-lg px-2 py-1.5 text-ui-label font-medium text-muted-foreground hover:bg-sidebar-accent hover:text-foreground transition-colors"
|
||||||
>
|
>
|
||||||
{expandedTeamGroups[group.key]
|
{expandedTeamGroups[group.key]
|
||||||
? "Weniger anzeigen"
|
? "Weniger anzeigen"
|
||||||
@ -3899,10 +3905,10 @@ export function ShellPage() {
|
|||||||
})}
|
})}
|
||||||
<div className="sidebar-projekte">
|
<div className="sidebar-projekte">
|
||||||
{/* PROJEKTE SECTION */}
|
{/* PROJEKTE SECTION */}
|
||||||
<div className="flex items-center justify-between pt-4 pb-1 mt-3 border-t border-sidebar-border px-2.5">
|
<div className="flex items-center justify-between pt-4 pb-1 mt-3 border-t border-white/5 px-2.5">
|
||||||
<div>
|
<div>
|
||||||
<div className="rk-shell-section-label">{t`Projekte`}</div>
|
<div className="rk-shell-section-label">{t`Projekte`}</div>
|
||||||
<p className="mt-0.5 px-0.5 text-ui-caption rk-sidebar-muted">
|
<p className="mt-0.5 px-0.5 text-ui-caption text-muted-foreground/70">
|
||||||
{t`Threads by topic · folders`}
|
{t`Threads by topic · folders`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -3912,14 +3918,14 @@ export function ShellPage() {
|
|||||||
aria-label={t`Create a project`}
|
aria-label={t`Create a project`}
|
||||||
data-testid="create-project-trigger"
|
data-testid="create-project-trigger"
|
||||||
onClick={() => openNewSpaceDialog("project")}
|
onClick={() => openNewSpaceDialog("project")}
|
||||||
className="rk-sidebar-muted hover:text-foreground/90 transition-colors"
|
className="text-muted-foreground/60 hover:text-foreground/90 transition-colors"
|
||||||
>
|
>
|
||||||
<Plus strokeWidth={2.5} className="size-icon-md" />
|
<Plus strokeWidth={2.5} className="size-icon-md" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{projectSidebarGroups.length === 0 ? (
|
{projectSidebarGroups.length === 0 ? (
|
||||||
<div className="p-4 text-center text-ui-label rk-sidebar-muted">
|
<div className="p-4 text-center text-ui-label text-muted-foreground">
|
||||||
<p>{t`No projects yet.`}</p>
|
<p>{t`No projects yet.`}</p>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -3957,7 +3963,7 @@ export function ShellPage() {
|
|||||||
</ProjectDragGrip>
|
</ProjectDragGrip>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex min-w-0 flex-1 items-center justify-between gap-2 rounded-lg pl-8 py-1 text-ui-label font-semibold tracking-wider uppercase text-foreground/65 dark:text-muted-foreground/80 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring"
|
className="flex min-w-0 flex-1 items-center justify-between gap-2 rounded-lg pl-8 py-1 text-ui-label font-semibold tracking-wider uppercase text-muted-foreground/60 hover:text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-ring"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (group.emptySpaceId) {
|
if (group.emptySpaceId) {
|
||||||
openSpaceChat(group.emptySpaceId, "/app");
|
openSpaceChat(group.emptySpaceId, "/app");
|
||||||
@ -4086,11 +4092,15 @@ export function ShellPage() {
|
|||||||
position: { x: event.clientX, y: event.clientY },
|
position: { x: event.clientX, y: event.clientY },
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className={rosterRowClass(
|
className={`flex w-full items-center gap-3 rounded-2xl px-3 py-3 mb-[2px] text-start transition-all duration-150 cursor-pointer min-h-[52px] ${
|
||||||
item.kind === "bot"
|
item.kind === "bot" && !inGroup && active?.id === item.chat.id
|
||||||
? !inGroup && active?.id === item.chat.id
|
? "bg-white/[0.09] shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_4px_16px_rgba(0,0,0,0.4)] border border-emerald-500/30"
|
||||||
: inGroup && activeGroup?.id === item.chat.id,
|
: item.kind === "group" &&
|
||||||
)}
|
inGroup &&
|
||||||
|
activeGroup?.id === item.chat.id
|
||||||
|
? "bg-white/[0.09] shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_4px_16px_rgba(0,0,0,0.4)] border border-emerald-500/30"
|
||||||
|
: "hover:bg-white/[0.05] border border-transparent"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{item.kind === "bot" ? (
|
{item.kind === "bot" ? (
|
||||||
<BotAvatar
|
<BotAvatar
|
||||||
@ -4107,30 +4117,34 @@ export function ShellPage() {
|
|||||||
<div className="flex min-w-0 items-center gap-2 overflow-hidden">
|
<div className="flex min-w-0 items-center gap-2 overflow-hidden">
|
||||||
<span
|
<span
|
||||||
dir="auto"
|
dir="auto"
|
||||||
className={`shrink-0 max-w-[170px] truncate text-ui-body tracking-tight ${rosterNameClass(
|
className={`shrink-0 max-w-[170px] truncate text-ui-body tracking-tight ${
|
||||||
item.chat.unread,
|
item.chat.unread
|
||||||
)}`}
|
? "font-bold text-white"
|
||||||
|
: "font-semibold text-white/95"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{item.chat.name}
|
{item.chat.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex shrink-0 items-center gap-1.5">
|
<div className="flex shrink-0 items-center gap-1.5">
|
||||||
<span className={rosterTimeClass}>
|
<span className="text-ui-body-sm text-zinc-400 tabular-nums font-medium">
|
||||||
{formatRosterTime(item.chat.updatedAt)}
|
{formatRosterTime(item.chat.updatedAt)}
|
||||||
</span>
|
</span>
|
||||||
{item.chat.unread ? (
|
{item.chat.unread ? (
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={rosterUnreadDotClass}
|
className="inline-block h-2 w-2 rounded-full bg-emerald-400"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
dir="auto"
|
dir="auto"
|
||||||
className={`mt-0.5 truncate text-ui-body leading-snug ${rosterPreviewClass(
|
className={`mt-0.5 truncate text-ui-body leading-snug ${
|
||||||
item.chat.unread,
|
item.chat.unread
|
||||||
)}`}
|
? "font-medium text-white"
|
||||||
|
: "text-zinc-300"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{cleanRosterPreview(item.chat.preview)}
|
{cleanRosterPreview(item.chat.preview)}
|
||||||
</div>
|
</div>
|
||||||
@ -4153,7 +4167,7 @@ export function ShellPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
aria-expanded={archivedOpen}
|
aria-expanded={archivedOpen}
|
||||||
onClick={() => setArchivedOpen((open) => !open)}
|
onClick={() => setArchivedOpen((open) => !open)}
|
||||||
className="flex w-full items-center justify-between rounded-lg px-2.5 py-2 text-ui-body rk-sidebar-muted hover:bg-sidebar-accent"
|
className="flex w-full items-center justify-between rounded-lg px-2.5 py-2 text-ui-body text-muted-foreground hover:bg-sidebar-accent"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<Trans>Archived</Trans>
|
<Trans>Archived</Trans>
|
||||||
@ -4342,7 +4356,7 @@ export function ShellPage() {
|
|||||||
<main
|
<main
|
||||||
aria-hidden={mobileSidebarOpen || undefined}
|
aria-hidden={mobileSidebarOpen || undefined}
|
||||||
inert={mobileSidebarOpen}
|
inert={mobileSidebarOpen}
|
||||||
className="flex min-w-0 flex-1 flex-col bg-background md:rounded-2xl md:border md:border-border md:shadow-2xl overflow-hidden transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)]"
|
className="flex min-w-0 flex-1 flex-col bg-background md:rounded-2xl md:border md:border-white/10 md:bg-[#0A0A0C] md:shadow-2xl overflow-hidden transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)]"
|
||||||
>
|
>
|
||||||
{activeNavPillar === "heute" ? (
|
{activeNavPillar === "heute" ? (
|
||||||
<div className="flex h-full flex-1 flex-col overflow-hidden">
|
<div className="flex h-full flex-1 flex-col overflow-hidden">
|
||||||
@ -4704,7 +4718,7 @@ export function ShellPage() {
|
|||||||
? rightSidebarWidth
|
? rightSidebarWidth
|
||||||
: 0,
|
: 0,
|
||||||
}}
|
}}
|
||||||
className={`absolute inset-y-0 end-0 z-20 flex min-h-0 shrink-0 flex-col overflow-hidden bg-background md:rounded-2xl md:border md:border-border md:shadow-2xl md:relative ${
|
className={`absolute inset-y-0 end-0 z-20 flex min-h-0 shrink-0 flex-col overflow-hidden bg-background md:rounded-2xl md:border md:border-white/10 md:bg-[#0A0A0C] md:shadow-2xl md:relative ${
|
||||||
panel &&
|
panel &&
|
||||||
(
|
(
|
||||||
active ||
|
active ||
|
||||||
@ -4808,8 +4822,8 @@ export function ShellPage() {
|
|||||||
/>
|
/>
|
||||||
{isDesktop ? (
|
{isDesktop ? (
|
||||||
devServerRunning ? (
|
devServerRunning ? (
|
||||||
<span className="inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-ui-label font-medium text-foreground">
|
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-500/10 px-2 py-0.5 text-ui-label font-medium text-emerald-400">
|
||||||
<span className="size-1.5 rounded-full bg-success" />
|
<span className="size-1.5 rounded-full bg-emerald-400" />
|
||||||
<Trans>Dev</Trans>
|
<Trans>Dev</Trans>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
@ -4984,11 +4998,11 @@ export function ShellPage() {
|
|||||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="absolute inset-0 bg-overlay"
|
className="absolute inset-0 bg-black/60"
|
||||||
aria-label={t`Close`}
|
aria-label={t`Close`}
|
||||||
onClick={() => setShowCreateBotModal(false)}
|
onClick={() => setShowCreateBotModal(false)}
|
||||||
/>
|
/>
|
||||||
<div className="relative z-10 max-h-[90vh] w-[480px] overflow-y-auto rounded-2xl border border-border bg-popover p-6 text-popover-foreground shadow-2xl">
|
<div className="relative z-10 max-h-[90vh] w-[480px] overflow-y-auto rounded-2xl border border-white/10 bg-[#0A0A0C] p-6 shadow-2xl">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<h2 className="text-lg font-semibold text-foreground">
|
<h2 className="text-lg font-semibold text-foreground">
|
||||||
<Trans>Create new Bot</Trans>
|
<Trans>Create new Bot</Trans>
|
||||||
@ -6093,10 +6107,10 @@ const Transcript = memo(function Transcript({
|
|||||||
<div className="relative mb-5 flex items-center justify-center p-3">
|
<div className="relative mb-5 flex items-center justify-center p-3">
|
||||||
<EvooMascot size={76} pose="greeting" interactive trackPointer="hover" />
|
<EvooMascot size={76} pose="greeting" interactive trackPointer="hover" />
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-3xl font-bold tracking-tight text-foreground mb-2">
|
<h2 className="text-3xl font-bold tracking-tight text-white mb-2">
|
||||||
{t`Guten Tag.`} evOo {t`ist bereit.`}
|
{t`Guten Tag.`} <span className="text-emerald-400">evOo</span> {t`ist bereit.`}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="max-w-md text-ui-body leading-relaxed text-foreground/85 mb-8 font-normal">
|
<p className="max-w-md text-ui-body leading-relaxed text-zinc-300 mb-8 font-normal">
|
||||||
{activeName
|
{activeName
|
||||||
? t`Frag ${activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews.`
|
? t`Frag ${activeName} nach Marktchancen, autonomen Tasks oder Architektur-Reviews.`
|
||||||
: t`Wähle einen autonomen Agenten aus deinem Team oder starte direkt einen Prompt.`}
|
: t`Wähle einen autonomen Agenten aus deinem Team oder starte direkt einen Prompt.`}
|
||||||
@ -6107,13 +6121,13 @@ const Transcript = memo(function Transcript({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
onPromptSelect?.(t`⚡ Führe einen autonomen Portfolio- und Risiko-Check durch`);
|
onPromptSelect?.(t`⚡ Führe einen autonomen Portfolio- und Risiko-Check durch`);
|
||||||
}}
|
}}
|
||||||
className="apple-glass-card flex flex-col items-start p-4 rounded-2xl text-start transition-all hover:scale-[1.02] cursor-pointer"
|
className="apple-glass-card group flex flex-col items-start p-4 rounded-2xl text-start transition-all hover:scale-[1.02] hover:border-emerald-500/40 cursor-pointer"
|
||||||
>
|
>
|
||||||
<span className="text-xl mb-2">⚡</span>
|
<span className="text-xl mb-2">⚡</span>
|
||||||
<span className="text-[14.5px] font-semibold text-foreground">
|
<span className="text-[14.5px] font-semibold text-white group-hover:text-emerald-400 transition-colors">
|
||||||
{t`Portfolio-Check`}
|
{t`Portfolio-Check`}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-ui-body-sm text-muted-foreground mt-1 leading-snug">
|
<span className="text-ui-body-sm text-zinc-400 mt-1 leading-snug">
|
||||||
{t`Echtzeit-Risiko & Drawdown-Analyse`}
|
{t`Echtzeit-Risiko & Drawdown-Analyse`}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@ -6122,13 +6136,13 @@ const Transcript = memo(function Transcript({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
onPromptSelect?.(t`📈 Scanne den Markt nach aktuellen Alpha-Signalen`);
|
onPromptSelect?.(t`📈 Scanne den Markt nach aktuellen Alpha-Signalen`);
|
||||||
}}
|
}}
|
||||||
className="apple-glass-card flex flex-col items-start p-4 rounded-2xl text-start transition-all hover:scale-[1.02] cursor-pointer"
|
className="apple-glass-card group flex flex-col items-start p-4 rounded-2xl text-start transition-all hover:scale-[1.02] hover:border-sky-500/40 cursor-pointer"
|
||||||
>
|
>
|
||||||
<span className="text-xl mb-2">📈</span>
|
<span className="text-xl mb-2">📈</span>
|
||||||
<span className="text-[14.5px] font-semibold text-foreground">
|
<span className="text-[14.5px] font-semibold text-white group-hover:text-sky-400 transition-colors">
|
||||||
{t`Alpha-Signale`}
|
{t`Alpha-Signale`}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-ui-body-sm text-muted-foreground mt-1 leading-snug">
|
<span className="text-ui-body-sm text-zinc-400 mt-1 leading-snug">
|
||||||
{t`Volatilität & Orderbuch-Liquidität`}
|
{t`Volatilität & Orderbuch-Liquidität`}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@ -6137,13 +6151,13 @@ const Transcript = memo(function Transcript({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
onPromptSelect?.(t`💎 Führe ein Steve Jobs UX & Architektur-Audit durch`);
|
onPromptSelect?.(t`💎 Führe ein Steve Jobs UX & Architektur-Audit durch`);
|
||||||
}}
|
}}
|
||||||
className="apple-glass-card flex flex-col items-start p-4 rounded-2xl text-start transition-all hover:scale-[1.02] cursor-pointer"
|
className="apple-glass-card group flex flex-col items-start p-4 rounded-2xl text-start transition-all hover:scale-[1.02] hover:border-purple-500/40 cursor-pointer"
|
||||||
>
|
>
|
||||||
<span className="text-xl mb-2">💎</span>
|
<span className="text-xl mb-2">💎</span>
|
||||||
<span className="text-[14.5px] font-semibold text-foreground">
|
<span className="text-[14.5px] font-semibold text-white group-hover:text-purple-400 transition-colors">
|
||||||
{t`Steve Jobs Audit`}
|
{t`Steve Jobs Audit`}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-ui-body-sm text-muted-foreground mt-1 leading-snug">
|
<span className="text-ui-body-sm text-zinc-400 mt-1 leading-snug">
|
||||||
{t`Schlichtheit, Eleganz & Detailtiefe`}
|
{t`Schlichtheit, Eleganz & Detailtiefe`}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@ -6719,7 +6733,7 @@ const Composer = memo(function Composer({
|
|||||||
{/* Quick Action Prompt Chips */}
|
{/* Quick Action Prompt Chips */}
|
||||||
{showComposerPlaceholder ? (
|
{showComposerPlaceholder ? (
|
||||||
<div className="mb-3 flex items-center gap-2 overflow-x-auto no-scrollbar py-1">
|
<div className="mb-3 flex items-center gap-2 overflow-x-auto no-scrollbar py-1">
|
||||||
<span className="flex items-center gap-1.5 text-ui-label font-semibold uppercase tracking-wider text-muted-foreground shrink-0 px-2.5 py-1 rounded-full bg-muted/40 border border-border me-1 shadow-xs">
|
<span className="flex items-center gap-1.5 text-ui-label font-semibold uppercase tracking-wider text-emerald-400 shrink-0 px-2.5 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/20 me-1 shadow-xs">
|
||||||
<Sparkles className="text-amber-400 size-icon-sm" />
|
<Sparkles className="text-amber-400 size-icon-sm" />
|
||||||
Quick
|
Quick
|
||||||
</span>
|
</span>
|
||||||
@ -6731,7 +6745,7 @@ const Composer = memo(function Composer({
|
|||||||
updateDraft(promptText);
|
updateDraft(promptText);
|
||||||
focusComposer();
|
focusComposer();
|
||||||
}}
|
}}
|
||||||
className="shrink-0 rounded-full border border-border bg-muted/40 px-3.5 py-1.5 text-ui-body-sm font-medium text-foreground/85 hover:bg-accent hover:text-foreground transition-all shadow-sm active:scale-95 cursor-pointer"
|
className="shrink-0 rounded-full border border-white/[0.1] bg-white/[0.05] px-3.5 py-1.5 text-ui-body-sm font-medium text-zinc-200 hover:border-emerald-500/40 hover:bg-white/[0.1] hover:text-white transition-all shadow-sm active:scale-95 cursor-pointer"
|
||||||
>
|
>
|
||||||
{promptText}
|
{promptText}
|
||||||
</button>
|
</button>
|
||||||
@ -6914,7 +6928,7 @@ const Composer = memo(function Composer({
|
|||||||
) : null}
|
) : null}
|
||||||
<div
|
<div
|
||||||
data-testid="composer-bar"
|
data-testid="composer-bar"
|
||||||
className="apple-island relative flex items-center gap-3.5 rounded-[30px] py-3 pe-3.5 ps-4 transition-all duration-200"
|
className="apple-island relative flex items-center gap-3.5 rounded-[30px] py-3 pe-3.5 ps-4 transition-all duration-200 focus-within:border-emerald-500/40 focus-within:shadow-[0_20px_50px_-10px_rgba(0,0,0,0.85),inset_0_1px_0_rgba(255,255,255,0.22),0_0_0_1px_rgba(16,185,129,0.35)] shadow-[0_16px_40px_-8px_rgba(0,0,0,0.7),inset_0_1px_0_rgba(255,255,255,0.14)]"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
@ -6930,7 +6944,7 @@ const Composer = memo(function Composer({
|
|||||||
aria-label={t`Attach file`}
|
aria-label={t`Attach file`}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={() => fileInputRef.current?.click()}
|
onClick={() => fileInputRef.current?.click()}
|
||||||
className="tactile-press size-10 shrink-0 rounded-full border border-border bg-foreground/5 text-foreground/80 transition-all hover:bg-foreground/10 hover:text-foreground flex items-center justify-center cursor-pointer"
|
className="tactile-press size-10 shrink-0 rounded-full border border-white/10 bg-white/[0.06] text-foreground/80 transition-all hover:bg-white/[0.12] hover:text-white flex items-center justify-center cursor-pointer"
|
||||||
>
|
>
|
||||||
<Plus strokeWidth={2.2} className="size-icon-lg" />
|
<Plus strokeWidth={2.2} className="size-icon-lg" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -6938,9 +6952,9 @@ const Composer = memo(function Composer({
|
|||||||
{selectedSkill ? (
|
{selectedSkill ? (
|
||||||
<span
|
<span
|
||||||
data-testid="skill-chip"
|
data-testid="skill-chip"
|
||||||
className="inline-flex max-w-full items-center gap-1.5 rounded-full border border-border bg-foreground/[0.08] px-3 py-1 text-ui-body font-medium text-foreground backdrop-blur-md"
|
className="inline-flex max-w-full items-center gap-1.5 rounded-full border border-white/10 bg-white/[0.08] px-3 py-1 text-ui-body font-medium text-foreground backdrop-blur-md"
|
||||||
>
|
>
|
||||||
<Box strokeWidth={1.8} className="shrink-0 text-muted-foreground size-icon-md" />
|
<Box strokeWidth={1.8} className="shrink-0 text-blue-400 size-icon-md" />
|
||||||
<span dir="auto" className="truncate">
|
<span dir="auto" className="truncate">
|
||||||
{selectedSkill.name}
|
{selectedSkill.name}
|
||||||
</span>
|
</span>
|
||||||
@ -6959,7 +6973,7 @@ const Composer = memo(function Composer({
|
|||||||
key={mentionChipKey(mention)}
|
key={mentionChipKey(mention)}
|
||||||
data-testid="mention-chip"
|
data-testid="mention-chip"
|
||||||
data-mention-kind={mention.kind}
|
data-mention-kind={mention.kind}
|
||||||
className="inline-flex max-w-full items-center gap-1.5 rounded-full border border-border bg-foreground/[0.08] px-3 py-1 text-ui-body font-medium text-foreground backdrop-blur-md"
|
className="inline-flex max-w-full items-center gap-1.5 rounded-full border border-white/10 bg-white/[0.08] px-3 py-1 text-ui-body font-medium text-foreground backdrop-blur-md"
|
||||||
>
|
>
|
||||||
<MentionChipIcon mention={mention} />
|
<MentionChipIcon mention={mention} />
|
||||||
<span dir="auto" className="truncate">
|
<span dir="auto" className="truncate">
|
||||||
@ -7039,7 +7053,7 @@ const Composer = memo(function Composer({
|
|||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
rows={1}
|
rows={1}
|
||||||
className="max-h-36 min-h-[28px] min-w-[8rem] flex-1 resize-none overflow-y-auto bg-transparent py-1 text-[16.5px] leading-[1.55] text-foreground outline-none placeholder:text-muted-foreground/75 disabled:opacity-40 font-normal"
|
className="max-h-36 min-h-[28px] min-w-[8rem] flex-1 resize-none overflow-y-auto bg-transparent py-1 text-[16.5px] leading-[1.55] text-foreground outline-none placeholder:text-muted-foreground/75 disabled:opacity-40 font-normal selection:bg-emerald-500/30"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{onVoice ? (
|
{onVoice ? (
|
||||||
@ -7072,7 +7086,7 @@ const Composer = memo(function Composer({
|
|||||||
aria-label={t`Send`}
|
aria-label={t`Send`}
|
||||||
disabled={!canSend || disabled}
|
disabled={!canSend || disabled}
|
||||||
onClick={send}
|
onClick={send}
|
||||||
className="tactile-press size-10 rounded-full transition-transform active:scale-90 flex items-center justify-center cursor-pointer"
|
className="tactile-press size-10 rounded-full bg-emerald-400 text-slate-950 hover:bg-emerald-300 shadow-[0_0_16px_rgba(16,185,129,0.35)] transition-transform active:scale-90 flex items-center justify-center cursor-pointer"
|
||||||
>
|
>
|
||||||
<ArrowUp strokeWidth={2.6} className="size-icon-lg" />
|
<ArrowUp strokeWidth={2.6} className="size-icon-lg" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -7082,7 +7096,7 @@ const Composer = memo(function Composer({
|
|||||||
aria-label={t`Stop`}
|
aria-label={t`Stop`}
|
||||||
disabled={sending}
|
disabled={sending}
|
||||||
onClick={() => void onStop()}
|
onClick={() => void onStop()}
|
||||||
className="tactile-press size-10 rounded-full border border-border bg-foreground/[0.08] text-foreground/90 shadow-sm transition-colors hover:bg-foreground/15 hover:text-foreground flex items-center justify-center cursor-pointer"
|
className="tactile-press size-10 rounded-full border border-white/15 bg-white/[0.08] text-foreground/90 shadow-sm transition-colors hover:bg-white/[0.15] hover:text-white flex items-center justify-center cursor-pointer"
|
||||||
>
|
>
|
||||||
<Square strokeWidth={0} fill="currentColor" className="size-icon-sm" />
|
<Square strokeWidth={0} fill="currentColor" className="size-icon-sm" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -7093,7 +7107,7 @@ const Composer = memo(function Composer({
|
|||||||
aria-label={t`Send`}
|
aria-label={t`Send`}
|
||||||
disabled={!canSend || disabled}
|
disabled={!canSend || disabled}
|
||||||
onClick={send}
|
onClick={send}
|
||||||
className="tactile-press size-10 shrink-0 rounded-full transition-all active:scale-90 disabled:bg-foreground/[0.08] disabled:text-foreground/20 flex items-center justify-center cursor-pointer"
|
className="tactile-press size-10 shrink-0 rounded-full bg-emerald-400 text-slate-950 hover:bg-emerald-300 shadow-[0_0_16px_rgba(16,185,129,0.35)] transition-all active:scale-90 disabled:bg-white/[0.08] disabled:text-white/20 disabled:shadow-none flex items-center justify-center cursor-pointer"
|
||||||
>
|
>
|
||||||
<ArrowUp strokeWidth={2.6} className="size-icon-lg" />
|
<ArrowUp strokeWidth={2.6} className="size-icon-lg" />
|
||||||
</Button>
|
</Button>
|
||||||
@ -7490,22 +7504,19 @@ const MessageView = memo(function MessageView({
|
|||||||
{effectiveSpeakerName && message.role === "bot" && !encounterMode ? (
|
{effectiveSpeakerName && message.role === "bot" && !encounterMode ? (
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<div className="mb-2 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<div className="relative flex items-center justify-center size-7 rounded-xl bg-foreground/[0.08] border border-border shadow-xs">
|
<div className="relative flex items-center justify-center size-7 rounded-xl bg-white/[0.08] border border-white/[0.12] shadow-xs">
|
||||||
<EvooMascot size={22} interactive={false} pose={isLive ? "working" : "idle"} />
|
<EvooMascot size={22} interactive={false} pose={isLive ? "working" : "idle"} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span
|
<span className="text-[14.5px] font-semibold text-white tracking-tight" dir="auto">
|
||||||
className="text-[14.5px] font-semibold text-foreground tracking-tight"
|
|
||||||
dir="auto"
|
|
||||||
>
|
|
||||||
{effectiveSpeakerName}
|
{effectiveSpeakerName}
|
||||||
</span>
|
</span>
|
||||||
<span className="rounded-full border border-border bg-muted px-2 py-0.5 text-ui-label font-medium text-muted-foreground">
|
<span className="rounded-full bg-emerald-500/15 border border-emerald-500/25 px-2 py-0.5 text-ui-label font-medium text-emerald-400">
|
||||||
Oo
|
Oo
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-ui-label font-mono text-muted-foreground tabular-nums">
|
<span className="text-ui-label font-mono text-zinc-400 tabular-nums">
|
||||||
{new Date(message.createdAt).toLocaleTimeString([], {
|
{new Date(message.createdAt).toLocaleTimeString([], {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
@ -7861,8 +7872,8 @@ const MessageView = memo(function MessageView({
|
|||||||
data-testid="message-user-bubble"
|
data-testid="message-user-bubble"
|
||||||
className={
|
className={
|
||||||
encounterMode
|
encounterMode
|
||||||
? "max-w-full whitespace-pre-wrap wrap-anywhere rounded-2xl bg-chat-user px-3.5 py-2 text-ui-body-sm leading-relaxed text-chat-user-foreground"
|
? "max-w-full whitespace-pre-wrap wrap-anywhere rounded-2xl border border-white/10 bg-white/[0.06] px-3.5 py-2 text-ui-body-sm leading-relaxed text-foreground/95"
|
||||||
: "chat-bubble-user max-w-full whitespace-pre-wrap wrap-anywhere rounded-[24px] px-6 py-4 text-[17px] leading-[1.62] text-chat-user-foreground shadow-md"
|
: "chat-bubble-user max-w-full whitespace-pre-wrap wrap-anywhere rounded-[24px] px-6 py-4 text-[17px] leading-[1.62] text-[#FAFAFA] shadow-md"
|
||||||
}
|
}
|
||||||
dir="auto"
|
dir="auto"
|
||||||
>
|
>
|
||||||
@ -7884,7 +7895,7 @@ const MessageView = memo(function MessageView({
|
|||||||
dir="auto"
|
dir="auto"
|
||||||
>
|
>
|
||||||
<ChatMarkdown>{block.text}</ChatMarkdown>
|
<ChatMarkdown>{block.text}</ChatMarkdown>
|
||||||
<div className="mt-3 pt-2.5 flex items-center justify-between border-t border-border text-ui-label text-muted-foreground/75">
|
<div className="mt-3 pt-2.5 flex items-center justify-between border-t border-white/[0.08] text-ui-label text-muted-foreground/75">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -7893,7 +7904,7 @@ const MessageView = memo(function MessageView({
|
|||||||
navigator.clipboard.writeText(block.text);
|
navigator.clipboard.writeText(block.text);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="inline-flex items-center gap-1.5 hover:text-foreground transition-colors cursor-pointer text-ui-body-sm font-medium"
|
className="inline-flex items-center gap-1.5 hover:text-white transition-colors cursor-pointer text-ui-body-sm font-medium"
|
||||||
title={t`Antwort kopieren`}
|
title={t`Antwort kopieren`}
|
||||||
>
|
>
|
||||||
<Copy size={13.5} />
|
<Copy size={13.5} />
|
||||||
@ -7904,14 +7915,14 @@ const MessageView = memo(function MessageView({
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label={speaking ? t`Stop speaking` : t`Speak this reply`}
|
aria-label={speaking ? t`Stop speaking` : t`Speak this reply`}
|
||||||
onClick={onSpeak}
|
onClick={onSpeak}
|
||||||
className="inline-flex items-center gap-1.5 hover:text-foreground transition-colors cursor-pointer text-ui-body-sm font-medium"
|
className="inline-flex items-center gap-1.5 hover:text-white transition-colors cursor-pointer text-ui-body-sm font-medium"
|
||||||
>
|
>
|
||||||
<Volume2 size={13.5} />
|
<Volume2 size={13.5} />
|
||||||
<span>{speaking ? t`Stop` : t`Vorlesen`}</span>
|
<span>{speaking ? t`Stop` : t`Vorlesen`}</span>
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<span className="tabular-nums font-mono text-ui-label text-muted-foreground">
|
<span className="tabular-nums font-mono text-ui-label text-zinc-400">
|
||||||
{new Date(message.createdAt).toLocaleTimeString([], {
|
{new Date(message.createdAt).toLocaleTimeString([], {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
@ -7929,7 +7940,7 @@ const MessageView = memo(function MessageView({
|
|||||||
{block.lines.map((line) => (
|
{block.lines.map((line) => (
|
||||||
<div key={line.k} className="flex items-baseline gap-2.5 text-ui-body">
|
<div key={line.k} className="flex items-baseline gap-2.5 text-ui-body">
|
||||||
<span className="text-success">✓</span>
|
<span className="text-success">✓</span>
|
||||||
<span className="font-semibold text-foreground">{line.k}</span>
|
<span className="font-semibold text-white">{line.k}</span>
|
||||||
<span className="text-muted-foreground">→</span>
|
<span className="text-muted-foreground">→</span>
|
||||||
<span>{line.v}</span>
|
<span>{line.v}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -441,7 +441,7 @@ export function BotSettings({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<label htmlFor={`${ids}-name`} className={fieldLabelClass}>
|
<label htmlFor={`${ids}-name`} className="mt-4 block text-ui-body text-muted-foreground/80">
|
||||||
<Trans>Name</Trans>
|
<Trans>Name</Trans>
|
||||||
<Input
|
<Input
|
||||||
id={`${ids}-name`}
|
id={`${ids}-name`}
|
||||||
@ -484,7 +484,10 @@ export function BotSettings({
|
|||||||
>
|
>
|
||||||
<Trans>Notifications</Trans>
|
<Trans>Notifications</Trans>
|
||||||
</div>
|
</div>
|
||||||
<div id={`${ids}-notify-finish-desc`} className="text-ui-body-sm text-muted-foreground">
|
<div
|
||||||
|
id={`${ids}-notify-finish-desc`}
|
||||||
|
className="text-ui-body-sm text-muted-foreground/70"
|
||||||
|
>
|
||||||
<Trans>Get notified when this Bot finishes or needs input</Trans>
|
<Trans>Get notified when this Bot finishes or needs input</Trans>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -428,7 +428,7 @@ export function ChartBlockView({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="xs"
|
size="xs"
|
||||||
onClick={() => setExpanded(true)}
|
onClick={() => setExpanded(true)}
|
||||||
className="absolute end-3 top-3 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 rounded-full"
|
className="absolute end-3 top-3 opacity-0 transition-opacity group-hover:opacity-100 focus-visible:opacity-100 rounded-full border-white/15 bg-white/10 text-white backdrop-blur-md"
|
||||||
>
|
>
|
||||||
<Trans>Expand</Trans>
|
<Trans>Expand</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,20 +1,20 @@
|
|||||||
/** One look for sidebar conversation rows: the main conversation entry and the bot roster. */
|
/** One look for sidebar conversation rows: the main conversation entry and the bot roster. */
|
||||||
export function rosterRowClass(selected: boolean): string {
|
export function rosterRowClass(selected: boolean): string {
|
||||||
return `flex w-full items-center gap-3 rounded-2xl border px-3 py-3 mb-[2px] text-start transition-all duration-150 cursor-pointer min-h-[52px] ${
|
return `flex w-full items-center gap-3 rounded-2xl px-3 py-3 mb-[2px] text-start transition-all duration-150 cursor-pointer min-h-[52px] ${
|
||||||
selected
|
selected
|
||||||
? "border-sidebar-border bg-sidebar-accent shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_4px_16px_rgba(0,0,0,0.4)]"
|
? "bg-white/[0.09] shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_4px_16px_rgba(0,0,0,0.4)] border border-emerald-500/30"
|
||||||
: "border-transparent hover:bg-sidebar-accent/50"
|
: "hover:bg-white/[0.05] border border-transparent"
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function rosterNameClass(unread: boolean): string {
|
export function rosterNameClass(unread: boolean): string {
|
||||||
return unread ? "font-bold text-sidebar-foreground" : "font-semibold text-sidebar-foreground";
|
return unread ? "font-bold text-white" : "font-semibold text-white/95";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rosterTimeClass = "text-ui-body-sm rk-sidebar-muted tabular-nums font-medium";
|
export const rosterTimeClass = "text-ui-body-sm text-zinc-400 tabular-nums font-medium";
|
||||||
|
|
||||||
export const rosterUnreadDotClass = "inline-block h-2 w-2 rounded-full bg-sidebar-foreground";
|
export const rosterUnreadDotClass = "inline-block h-2 w-2 rounded-full bg-emerald-400";
|
||||||
|
|
||||||
export function rosterPreviewClass(unread: boolean): string {
|
export function rosterPreviewClass(unread: boolean): string {
|
||||||
return unread ? "font-medium text-sidebar-foreground" : "text-sidebar-foreground/85";
|
return unread ? "font-medium text-white" : "text-zinc-300";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,13 @@ body,
|
|||||||
#root {
|
#root {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: var(--background);
|
background: #07090e;
|
||||||
|
background-image: radial-gradient(
|
||||||
|
circle at 50% 0%,
|
||||||
|
rgba(16, 185, 129, 0.07) 0%,
|
||||||
|
rgba(14, 165, 233, 0.04) 40%,
|
||||||
|
transparent 80%
|
||||||
|
);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-family:
|
font-family:
|
||||||
"Inter Variable", -apple-system, BlinkMacSystemFont, "SF Pro Display", "Geist Variable",
|
"Inter Variable", -apple-system, BlinkMacSystemFont, "SF Pro Display", "Geist Variable",
|
||||||
@ -35,8 +41,8 @@ body,
|
|||||||
.apple-island {
|
.apple-island {
|
||||||
backdrop-filter: blur(36px) saturate(210%);
|
backdrop-filter: blur(36px) saturate(210%);
|
||||||
-webkit-backdrop-filter: blur(36px) saturate(210%);
|
-webkit-backdrop-filter: blur(36px) saturate(210%);
|
||||||
background: color-mix(in srgb, var(--card) 88%, transparent);
|
background: rgba(12, 16, 24, 0.88);
|
||||||
border: 1px solid color-mix(in srgb, var(--foreground) 13%, transparent);
|
border: 1px solid rgba(255, 255, 255, 0.13);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 18px 44px -8px rgba(0, 0, 0, 0.75),
|
0 18px 44px -8px rgba(0, 0, 0, 0.75),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.16);
|
inset 0 1px 0 rgba(255, 255, 255, 0.16);
|
||||||
@ -45,24 +51,24 @@ body,
|
|||||||
.apple-glass-card {
|
.apple-glass-card {
|
||||||
backdrop-filter: blur(20px);
|
backdrop-filter: blur(20px);
|
||||||
-webkit-backdrop-filter: blur(20px);
|
-webkit-backdrop-filter: blur(20px);
|
||||||
background: color-mix(in srgb, var(--card) 68%, transparent);
|
background: rgba(16, 21, 30, 0.68);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid rgba(255, 255, 255, 0.09);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
||||||
0 6px 24px rgba(0, 0, 0, 0.35);
|
0 6px 24px rgba(0, 0, 0, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-user {
|
.chat-bubble-user {
|
||||||
background: var(--chat-user);
|
background: linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(15, 23, 42, 0.98) 100%);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 8px 24px -4px rgba(0, 0, 0, 0.5),
|
0 8px 24px -4px rgba(0, 0, 0, 0.5),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
inset 0 1px 0 rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-bubble-bot {
|
.chat-bubble-bot {
|
||||||
background: color-mix(in srgb, var(--card) 72%, transparent);
|
background: rgba(15, 20, 29, 0.72);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 10px 32px -4px rgba(0, 0, 0, 0.35),
|
0 10px 32px -4px rgba(0, 0, 0, 0.35),
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||||
@ -73,7 +79,7 @@ body,
|
|||||||
/* Shell layout utilities — token-backed, use instead of one-off pixel classes */
|
/* Shell layout utilities — token-backed, use instead of one-off pixel classes */
|
||||||
@layer components {
|
@layer components {
|
||||||
.rk-shell-section-label {
|
.rk-shell-section-label {
|
||||||
@apply text-ui-label font-bold tracking-wider uppercase text-foreground/65 dark:text-muted-foreground/80;
|
@apply text-ui-label font-bold tracking-wider uppercase text-muted-foreground/80;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rk-shell-icon-btn {
|
.rk-shell-icon-btn {
|
||||||
@ -84,23 +90,16 @@ body,
|
|||||||
@apply text-ui-body-sm text-muted-foreground;
|
@apply text-ui-body-sm text-muted-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Secondary sidebar text and icons. Muted text measures 4.4:1 on the light sidebar;
|
|
||||||
a dimmed foreground stays above 4.5:1. */
|
|
||||||
.rk-sidebar-muted {
|
|
||||||
@apply text-sidebar-foreground/65;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rk-shell-row-title {
|
.rk-shell-row-title {
|
||||||
@apply text-ui-body font-medium text-foreground;
|
@apply text-ui-body font-medium text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Gemini-Grade Accessible Typography (Apple-like with Inter Variable).
|
/* Gemini-Grade Accessible Typography (Apple-like with Inter Variable) */
|
||||||
Heading, code and quote colors come from the shared @rakazo/chat-ui markdown styles. */
|
|
||||||
.rk-chat-markdown {
|
.rk-chat-markdown {
|
||||||
font-size: var(--font-size-reading);
|
font-size: var(--font-size-reading);
|
||||||
line-height: var(--line-height-reading);
|
line-height: var(--line-height-reading);
|
||||||
color: var(--foreground);
|
color: #f8fafc;
|
||||||
letter-spacing: -0.011em;
|
letter-spacing: -0.011em;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
@ -111,6 +110,8 @@ body,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rk-chat-markdown pre {
|
.rk-chat-markdown pre {
|
||||||
|
background: rgba(8, 11, 16, 0.96);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.11);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.09),
|
inset 0 1px 0 rgba(255, 255, 255, 0.09),
|
||||||
@ -127,6 +128,9 @@ body,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rk-chat-markdown code:not(pre code) {
|
.rk-chat-markdown code:not(pre code) {
|
||||||
|
background: rgba(255, 255, 255, 0.09);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.13);
|
||||||
|
color: var(--foreground);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 0.15em 0.4em;
|
padding: 0.15em 0.4em;
|
||||||
@ -139,6 +143,7 @@ body,
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
letter-spacing: -0.025em;
|
letter-spacing: -0.025em;
|
||||||
|
color: #ffffff;
|
||||||
margin: 1.6em 0 0.6em;
|
margin: 1.6em 0 0.6em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,6 +152,7 @@ body,
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
|
color: #ffffff;
|
||||||
margin: 1.4em 0 0.5em;
|
margin: 1.4em 0 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,6 +161,7 @@ body,
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
letter-spacing: -0.015em;
|
letter-spacing: -0.015em;
|
||||||
|
color: #ffffff;
|
||||||
margin: 1.2em 0 0.4em;
|
margin: 1.2em 0 0.4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,13 +177,17 @@ body,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rk-chat-markdown strong {
|
.rk-chat-markdown strong {
|
||||||
|
color: #ffffff;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rk-chat-markdown blockquote {
|
.rk-chat-markdown blockquote {
|
||||||
|
border-left: 3px solid #10b981;
|
||||||
padding-left: 1.2em;
|
padding-left: 1.2em;
|
||||||
margin: 1.3em 0;
|
margin: 1.3em 0;
|
||||||
|
color: #cbd5e1;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
background: rgba(16, 185, 129, 0.05);
|
||||||
border-radius: 0 8px 8px 0;
|
border-radius: 0 8px 8px 0;
|
||||||
padding-top: 0.4em;
|
padding-top: 0.4em;
|
||||||
padding-bottom: 0.4em;
|
padding-bottom: 0.4em;
|
||||||
@ -224,11 +235,11 @@ body,
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
.rk-scroll::-webkit-scrollbar-thumb {
|
.rk-scroll::-webkit-scrollbar-thumb {
|
||||||
background: var(--scrollbar);
|
background: rgba(255, 255, 255, 0.14);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
.rk-scroll::-webkit-scrollbar-thumb:hover {
|
.rk-scroll::-webkit-scrollbar-thumb:hover {
|
||||||
background: var(--scrollbar-hover);
|
background: rgba(255, 255, 255, 0.28);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-drag {
|
.app-drag {
|
||||||
|
|||||||
@ -38,27 +38,17 @@ describe("server image publish workflow", () => {
|
|||||||
expect(publish.needs).toBe("build");
|
expect(publish.needs).toBe("build");
|
||||||
expect(workflowText).toContain("push-by-digest=true");
|
expect(workflowText).toContain("push-by-digest=true");
|
||||||
expect(workflowText).toContain("docker buildx imagetools create");
|
expect(workflowText).toContain("docker buildx imagetools create");
|
||||||
expect(workflowText).toContain(`if [[ "\${#sources[@]}" -ne 1 ]]`);
|
expect(workflowText).toContain('if [[ "${#sources[@]}" -ne 1 ]]');
|
||||||
expect(workflowText).toContain("for want in linux/amd64; do");
|
expect(workflowText).toContain("for want in linux/amd64; do");
|
||||||
expect(workflowText).toContain("actions/attest-build-provenance@");
|
expect(workflowText).toContain("actions/attest-build-provenance@");
|
||||||
expect(workflowText).not.toContain("github.repository_owner_type");
|
|
||||||
expect(workflowText).toContain(`gh api "repos/\${GITHUB_REPOSITORY}"`);
|
|
||||||
expect(workflowText).toContain(".owner.type");
|
|
||||||
expect(workflowText).toContain("if: steps.attestation.outputs.enabled == 'true'");
|
|
||||||
expect(workflowText).toContain('[[ "$owner_type" == "User" && "$private" == "true" ]]');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("stores digest markers under RUNNER_TEMP so self-hosted runners do not leak stale files", () => {
|
|
||||||
expect(workflowText).not.toContain("/tmp/digests");
|
|
||||||
expect(workflowText).toContain("RUNNER_TEMP");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("names every image under the lowercase repository namespace", () => {
|
it("names every image under the lowercase repository namespace", () => {
|
||||||
expect(workflowText).not.toContain(`ghcr.io/\${{ github.repository }}`);
|
expect(workflowText).not.toContain("ghcr.io/${{ github.repository }}");
|
||||||
const references =
|
const references =
|
||||||
workflowText.match(/ghcr\.io\/\$\{\{ steps\.ns\.outputs\.repo \}\}\//g) ?? [];
|
workflowText.match(/ghcr\.io\/\$\{\{ steps\.ns\.outputs\.repo \}\}\//g) ?? [];
|
||||||
expect(references.length).toBeGreaterThanOrEqual(6);
|
expect(references.length).toBeGreaterThanOrEqual(6);
|
||||||
expect(workflowText).toContain(`repo=\${GITHUB_REPOSITORY,,}`);
|
expect(workflowText).toContain("repo=${GITHUB_REPOSITORY,,}");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps pull requests read-only and every action pinned to a commit", () => {
|
it("keeps pull requests read-only and every action pinned to a commit", () => {
|
||||||
|
|||||||
@ -157,6 +157,7 @@
|
|||||||
.rk-chat-markdown blockquote {
|
.rk-chat-markdown blockquote {
|
||||||
border-left: 3px solid var(--border);
|
border-left: 3px solid var(--border);
|
||||||
padding-left: 0.85em;
|
padding-left: 0.85em;
|
||||||
|
color: var(--muted-foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rk-chat-markdown table {
|
.rk-chat-markdown table {
|
||||||
|
|||||||
@ -49,7 +49,7 @@ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
|
|||||||
<TabsPrimitive.Tab
|
<TabsPrimitive.Tab
|
||||||
data-slot="tabs-trigger"
|
data-slot="tabs-trigger"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-ui-body-sm font-medium whitespace-nowrap text-muted-foreground transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-icon-md",
|
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-ui-body-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-icon-md",
|
||||||
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
|
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
|
||||||
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
|
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
|
||||||
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
|
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user