slack gui: getting-started card with animated how-it-works carousel

Split-scene carousel on the Slack connector page teaching mentions, threads, and approvals.
Post-it annotations per beat; static under prefers-reduced-motion.

Co-authored-by: Devika <devikaverma11@gmail.com>
This commit is contained in:
Rohit P
2026-07-21 12:56:36 -07:00
co-authored by Devika
parent 2451486609
commit 062fb9837e
6 changed files with 743 additions and 4 deletions
+4 -2
View File
@@ -358,8 +358,10 @@ export async function mockApi(page: import("@playwright/test").Page) {
account: "deeplearning.ai",
allowed_users: [] as string[], // flat list (manual Socket Mode only)
workspaces: [
{ team_id: "T1DL", account: "deeplearning.ai", domain: "dlaiteam", allowed_users: [] as string[], allow_all: false, allowed_user_names: {} as Record<string, string | null> },
{ team_id: "T2AC", account: "acme-partners", domain: "acmehq", allowed_users: [] as string[], allow_all: false, allowed_user_names: {} as Record<string, string | null> },
// T1DL mirrors a managed install: the installer (authed_user) was pre-added
// to the allow-list on connect (UX-027) — keys the "you" chip + setup card.
{ team_id: "T1DL", account: "deeplearning.ai", domain: "dlaiteam", allowed_users: ["U_ME"] as string[], allow_all: false, allowed_user_names: {} as Record<string, string | null>, installer_user_id: "U_ME", installer_name: "Rohit Prasad" },
{ team_id: "T2AC", account: "acme-partners", domain: "acmehq", allowed_users: [] as string[], allow_all: false, allowed_user_names: {} as Record<string, string | null>, installer_user_id: "", installer_name: "" },
],
};
const slackConnector = () => ({
+66
View File
@@ -0,0 +1,66 @@
// UX-027: the Slack post-connect orientation card — installer pre-added to the
// allow-list ("you" chip), status line, 3-tab animated how-it-works carousel
// (no "Listen to a channel" — deferred by owner call), collapse persisted locally.
import { expect } from "@playwright/test";
import { test } from "./fixtures";
async function openSlackPage(page) {
await page.goto("/");
await page.getByTestId("account-row").click();
await page.getByRole("button", { name: "Connectors", exact: true }).click();
await page.getByTestId("connector-slack").click();
}
test("post-connect card: personalized status line + the installer's 'you' chip", async ({
page,
}) => {
await openSlackPage(page);
const card = page.getByTestId("slack-howitworks");
await expect(card).toContainText("Getting started with Slack & OpenWorker");
await expect(card).toContainText("deeplearning.ai connected");
await expect(card).toContainText("you're on the People list");
// The pre-added installer renders as a named chip marked "you" in ITS workspace.
const chip = page.getByTestId("slack-workspace-T1DL").getByTestId("people-chip-you");
await expect(chip).toContainText("Rohit Prasad");
await expect(chip).toContainText("· you");
});
test("carousel has exactly the 3 shipped scenes and tabs switch the caption", async ({
page,
}) => {
await openSlackPage(page);
const card = page.getByTestId("slack-howitworks");
await expect(card.getByTestId("hiw-tab-0")).toContainText("Mention → session");
await expect(card.getByTestId("hiw-tab-1")).toContainText("Threads stay connected");
await expect(card.getByTestId("hiw-tab-2")).toContainText("Allow teammates");
await expect(card).not.toContainText("Listen to a channel"); // deferred (rev 4)
await expect(card.getByTestId("hiw-caption")).toContainText("a session opens here");
// rev 7: the post-it layer restates the concept in place
await expect(card.getByTestId("hiw-scene")).toContainText("a @mention starts a NEW session");
await card.getByTestId("hiw-tab-1").click();
await expect(card.getByTestId("hiw-caption")).toContainText("same session");
await expect(card.getByTestId("hiw-scene")).toContainText("2 replies");
await expect(card.getByTestId("hiw-scene")).toContainText("continues the SAME conversation");
await card.getByTestId("hiw-tab-2").click();
await expect(card.getByTestId("hiw-caption")).toContainText("waits for your OK");
await expect(card.getByTestId("hiw-scene")).toContainText("Allow & deliver");
});
test("collapse hides the carousel, keeps the status line, and survives a reload", async ({
page,
}) => {
await openSlackPage(page);
const card = page.getByTestId("slack-howitworks");
await expect(card.getByTestId("hiw-tab-0")).toBeVisible();
await card.getByTestId("hiw-collapse").click();
await expect(card.getByTestId("hiw-tab-0")).toHaveCount(0);
await expect(card).toContainText("you're on the People list"); // status line stays
await openSlackPage(page); // full re-navigation — the seen-state is local
await expect(page.getByTestId("slack-howitworks")).toBeVisible();
await expect(page.getByTestId("hiw-tab-0")).toHaveCount(0);
await page.getByTestId("hiw-collapse").click(); // reopen works
await expect(page.getByTestId("hiw-tab-0")).toBeVisible();
});
+4
View File
@@ -307,6 +307,10 @@ export interface SlackWorkspace {
allowed_users: string[];
allow_all: boolean;
allowed_user_names?: Record<string, string | null>;
// Who installed this workspace (authed_user) — pre-added to the allow-list on
// connect (UX-027); the GUI marks their chip "you" and keys the setup card copy.
installer_user_id?: string;
installer_name?: string;
}
// One connected GitHub App installation (managed relay is multi-installation;
@@ -17,6 +17,7 @@ import {
import { ConnectorBadge } from "../../connectors/ConnectorIcon";
import { AddConnectionModal } from "./AddConnectionModal";
import type { DetailProps } from "./ConnectorsSection";
import { SlackHowItWorks } from "./SlackHowItWorks";
import { ToolsDisclosure } from "./ToolsDisclosure";
import { FOOT, GRP, GRP_H, PILL_ACCENT, PILL_LINE, ROW, TAG_WARN, XBTN } from "./ui";
@@ -105,6 +106,10 @@ export function SlackDetail({ c, cloud, slack, onChanged }: DetailProps) {
</div>
)}
{/* UX-027: post-connect orientation status line + animated how-it-works
carousel (collapsible; collapsed state is the local "seen" flag). */}
{relay && workspaces.length > 0 && <SlackHowItWorks workspaces={workspaces} />}
{relay &&
workspaces.map((w) => (
<WorkspaceGroup
@@ -217,6 +222,8 @@ function WorkspaceGroup({
allowed={w.allowed_users}
names={w.allowed_user_names}
teamId={w.team_id}
installerId={w.installer_user_id}
installerName={w.installer_name}
onRemove={(u) => disallowUser("slack", u, w.team_id).then(onChanged)}
onChanged={onChanged}
/>
@@ -253,15 +260,23 @@ function PeopleRow({
allowed,
names,
teamId,
installerId,
installerName,
onRemove,
onChanged,
}: {
allowed: string[];
names?: Record<string, string | null>;
teamId: string | null; // null = manual flat list (directory queries as "default")
installerId?: string; // authed_user — pre-added on managed connect (UX-027)
installerName?: string;
onRemove: (userId: string) => void;
onChanged: () => void;
}) {
// The installer's chip reads "you" — their name may still be unresolved (it's
// fetched lazily for outbound attribution), so fall back to a literal "You".
const label = (u: string) =>
names?.[u] || (u === installerId ? installerName || "You" : u);
return (
<div className={ROW}>
<span className={LABEL}>People</span>
@@ -274,11 +289,13 @@ function PeopleRow({
key={u}
className="inline-flex items-center gap-1.5 pl-1 pr-2 py-0.5 rounded-full bg-paper border border-line text-[12.5px]"
title={`id ${u}`}
data-testid={u === installerId ? "people-chip-you" : undefined}
>
<span className="w-5 h-5 rounded-full bg-accentSoft text-accent grid place-items-center text-[9px] font-bold">
{initials(names?.[u] || u)}
{initials(label(u))}
</span>
{names?.[u] || u}
{label(u)}
{u === installerId && <span className="text-[10.5px] text-faint">· you</span>}
<button className={XBTN} title="remove" onClick={() => onRemove(u)}>
×
</button>
@@ -0,0 +1,466 @@
import { useEffect, useRef, useState } from "react";
import type { SlackWorkspace } from "../../api";
// UX-027: the post-connect "how mentions reach you" card. A tabbed carousel of
// animated split-scenes — Slack on the left (pinned to light-Slack colors, so it
// reads as a screenshot of Slack), OpenWorker on the right (app tokens). Tabs
// auto-advance through one full tour, then idle on a loop of the current scene;
// clicking a tab takes over. The chevron collapses the carousel to the status
// line — collapsed IS the seen-state (stored locally, survives restarts).
// "Listen to a channel" is deliberately absent: owner wants that scene reworked
// before it ships (UX-027 rev 4).
const KEY = "ocw.slack.howitworks.collapsed";
const DUR = 8000; // per-scene loop, ms
const TABS = ["Mention → session", "Threads stay connected", "Allow teammates"];
const CAPTIONS = [
"Mention @OpenWorker in any channel it's invited to — a session opens here, and the answer lands back in Slack as a thread.",
"Mention it again inside the thread — the conversation continues in the same session, context intact. The thread is the session.",
"Teammates aren't auto-trusted: their first mention waits for your OK, then they're on the People list.",
];
function readCollapsed(): boolean {
try { return localStorage.getItem(KEY) === "1"; } catch { return false; }
}
export function SlackHowItWorks({ workspaces }: { workspaces: SlackWorkspace[] }) {
const [collapsed, setCollapsed] = useState(readCollapsed);
const [tab, setTab] = useState(0);
const [cycle, setCycle] = useState(0); // bump = remount the scene = restart its animations
const tourRef = useRef(TABS.length); // auto-advances left in the one-time story tour
useEffect(() => {
if (collapsed) return;
const t = window.setTimeout(() => {
if (tourRef.current > 1) {
tourRef.current -= 1;
setTab((x) => (x + 1) % TABS.length);
} else {
tourRef.current = 0;
setCycle((c) => c + 1); // keep looping the current scene quietly
}
}, DUR);
return () => window.clearTimeout(t);
}, [tab, cycle, collapsed]);
const jump = (i: number) => {
tourRef.current = 0; // a click takes over: no more auto-advance
setTab(i);
setCycle((c) => c + 1);
};
const toggle = () => {
const v = !collapsed;
setCollapsed(v);
try { localStorage.setItem(KEY, v ? "1" : "0"); } catch { /* best effort */ }
};
// Personalize when the install pre-added the connecting user (setup.py):
// their workspace names the status line; the scenes call them by first name.
const mine = workspaces.find(
(w) => w.installer_user_id && w.allowed_users.includes(w.installer_user_id)
);
const ws = mine ?? workspaces[0];
const meName =
(mine &&
(mine.installer_name ||
mine.allowed_user_names?.[mine.installer_user_id ?? ""])) ||
"You";
const meFirst = meName.split(/\s+/)[0];
const meInitial = (meName[0] || "Y").toUpperCase();
return (
// Rev 7 (owner): BORDERLESS — a proper section title + quiet status line;
// the only boxes on screen are the two mini-windows, which own their frames.
<div className="mb-5" data-testid="slack-howitworks">
<div className="flex items-baseline gap-2.5">
<h3 className="text-[13.5px] font-semibold tracking-tight">
Getting started with Slack &amp; OpenWorker
</h3>
<button
className="ml-auto shrink-0 inline-flex items-center gap-1.5 text-[12px] text-muted hover:text-ink"
data-testid="hiw-collapse"
title={collapsed ? "Show how mentions work" : "Collapse — reopen anytime"}
onClick={toggle}
>
{collapsed ? "How it works" : "Hide"}
<span
className="text-[9px] transition-transform"
style={collapsed ? { transform: "rotate(-90deg)" } : undefined}
>
</span>
</button>
</div>
<div className="text-[12px] text-muted mt-0.5">
<span className="text-ok font-bold"> </span>
{ws?.account || "Workspace"} connected
{mine
? " — you're on the People list, so your mentions get through."
: " — here's how mentions reach you."}
</div>
{!collapsed && (
<div className="mt-3">
<div className="flex gap-1 border-b border-line mb-3">
{TABS.map((t, i) => (
<button
key={t}
className={"hiw-tab" + (i === tab ? " on" : "")}
data-testid={`hiw-tab-${i}`}
style={{ "--hiw-dur": `${DUR}ms` } as React.CSSProperties}
onClick={() => jump(i)}
>
{t}
<span className="hiw-prog"><i /></span>
</button>
))}
</div>
<div className="hiw-scene hiw-play" key={`${tab}:${cycle}`} data-testid="hiw-scene">
{tab === 0 && <SceneMention meFirst={meFirst} meInitial={meInitial} />}
{tab === 1 && <SceneThread meFirst={meFirst} meInitial={meInitial} />}
{tab === 2 && <SceneTeammates />}
</div>
<div className="mt-2.5 text-[12px] text-muted" data-testid="hiw-caption">
{CAPTIONS[tab]}
</div>
</div>
)}
</div>
);
}
/* ---- shared miniature furniture ---- */
// The scenes deliberately play in a FICTIONAL workspace ("Lumina Labs") — a real
// account name here (or anything resembling our own product) reads as confusing
// or fake in an educational animation; the card's status line above keeps the
// user's real workspace name.
const WS_NAME = "Lumina Labs";
/* Post-it notes (rev 7, owner-approved): the concept in five hand-written words,
slapped onto the scene at its beat; they fade out near the end of each loop so
they read as annotation, never as UI. */
function Sticky({
d: delay, r, pos, children,
}: {
d: string; r?: boolean; pos: React.CSSProperties; children: React.ReactNode;
}) {
return (
<div
className={"hiw-sticky hiw-k" + (r ? " r" : "")}
style={{ "--d": delay, ...pos } as React.CSSProperties}
>
{children}
</div>
);
}
const ThreadsIcon = () => (
<svg className="hiw-ic" viewBox="0 0 24 24" aria-hidden="true">
<path d="M21 11.5a8.5 8.5 0 1 1-4.7-7.6L21 3l-.9 4.7a8.5 8.5 0 0 1 .9 3.8z" />
<path d="M8 10h8M8 14h5" />
</svg>
);
const SendIcon = () => (
<svg className="hiw-ic" viewBox="0 0 24 24" aria-hidden="true">
<path d="M22 2 11 13" />
<path d="M22 2l-7 20-4-9-9-4 20-7z" />
</svg>
);
function SlackRail({ active }: { active: string }) {
return (
<div className="hiw-slrail">
<div className="hiw-ws">{WS_NAME} </div>
<div className="hiw-slnav"><ThreadsIcon /> Threads</div>
<div className="hiw-slnav"><SendIcon /> Drafts &amp; sent</div>
<div className="hiw-sect">Channels</div>
<div className={"hiw-ch" + (active === "general" ? " on" : "")}># general</div>
<div className={"hiw-ch" + (active === "launch-room" ? " on" : "")}># launch-room</div>
<div className="hiw-sect">Direct messages</div>
<div className="hiw-slnav"><span className="hiw-pres" />Priya N</div>
<div className="hiw-slnav"><span className="hiw-pres" />Emma W</div>
<div className="hiw-sect">Agents &amp; apps</div>
<div className="hiw-slnav"><span className="hiw-appav">OW</span>OpenWorker</div>
</div>
);
}
function SlackWin({ children }: { children: React.ReactNode }) {
return (
<div className="hiw-win hiw-sl">
<div className="hiw-sltop">
<span className="hiw-dots"><i /><i /><i /></span>
<span className="hiw-slsearch"> Describe what you are looking for</span>
</div>
<div className="hiw-slbody">{children}</div>
</div>
);
}
/* Slack's two-row message box: formatting toolbar, then + / placeholder / send. */
function SlackComposer({ placeholder }: { placeholder: string }) {
return (
<div className="hiw-slcomposer">
<div className="hiw-slctools">
<b>B</b><i>I</i><u>U</u><s>S</s><span></span><span></span><span></span><span>{"</>"}</span>
</div>
<div className="hiw-slcrow">
<span className="hiw-plus"></span> {placeholder}
<span className="hiw-send"></span>
</div>
</div>
);
}
function SlackDate({ label }: { label: string }) {
return (
<div className="hiw-sldate">
<span>{label} </span>
</div>
);
}
function OwWin({ children }: { children: React.ReactNode }) {
return (
<div className="hiw-win hiw-ow">
<div className="hiw-owtop">
<span className="hiw-dots"><i /><i /><i /></span> OpenWorker
</div>
<div className="hiw-owbody">{children}</div>
</div>
);
}
function OwRail({ hot, hotSub, glow }: { hot?: string; hotSub?: string; glow?: boolean }) {
return (
<div className="hiw-owrail">
<div className="hiw-brand">OpenWorker</div>
<div className="hiw-newbtn"> New session</div>
<div className="hiw-ownav"> Search</div>
<div className="hiw-ownav"> Automations</div>
<div className="hiw-sect">RECENT</div>
{hot && (
<div
className={"hiw-sess hot" + (glow ? " hiw-glow hiw-k" : " hiw-stay")}
style={glow ? ({ "--d": "2.5s", "--g": "2.9s" } as React.CSSProperties) : undefined}
>
<b>{hot}</b>
{hotSub}
</div>
)}
<div className="hiw-sess"><b>Jira vs Linear</b>Coworker</div>
</div>
);
}
const d = (delay: string, extra?: Record<string, string>) =>
({ "--d": delay, ...extra } as React.CSSProperties);
function Msg({
av, avBg, name, ts, app, children, delay, extra,
}: {
av: string; avBg: string; name: string; ts: string; app?: boolean;
children: React.ReactNode; delay?: string; extra?: React.ReactNode;
}) {
return (
<div className={"hiw-slm" + (delay ? " hiw-k" : "")} style={delay ? d(delay) : undefined}>
<span className="hiw-sav" style={{ background: avBg }}>{av}</span>
<span className="min-w-0">
<span className="hiw-nm">{name}{app && <span className="hiw-appb">APP</span>}</span>
<span className="hiw-ts">{ts}</span>
<br />
<span>{children}</span>
{extra}
</span>
</div>
);
}
/* ---- scene 1: mention in a channel → new session, reply via thread panel ---- */
function SceneMention({ meFirst, meInitial }: { meFirst: string; meInitial: string }) {
return (
<>
<span className="hiw-spark" style={d("1.9s")} />
<Sticky d="3.1s" pos={{ left: "51%", top: "8%" }}>a @mention starts a NEW session </Sticky>
<Sticky d="5.8s" r pos={{ left: "27%", bottom: "5%" }}>the answer comes back as a thread </Sticky>
<SlackWin>
<SlackRail active="launch-room" />
<div className="hiw-slmain">
<div className="hiw-slhead"># launch-room <span className="hiw-sub">· 24 members</span></div>
<div className="hiw-slmsgs">
<SlackDate label="Today" />
<Msg av="P" avBg="#7c6cd0" name="Priya N" ts="6:31 PM">
signups are spiking since the post 📈
</Msg>
<Msg
av={meInitial} avBg="#3b82c4" name={meFirst} ts="6:33 PM" delay=".8s"
extra={
<span className="hiw-replybar hiw-k" style={d("4.6s")}>
<span className="hiw-sav2">OW</span> 1 reply
<span className="hiw-later">Today at 6:34 PM</span>
</span>
}
>
<span className="hiw-men">@OpenWorker</span> summarize this thread
</Msg>
</div>
<SlackComposer placeholder="Message #launch-room" />
<div className="hiw-slthread hiw-k" style={d("5.1s")}>
<div className="hiw-th">Thread <span className="hiw-sub"># launch-room</span><span className="hiw-x"></span></div>
<div className="hiw-tmsgs">
<Msg av={meInitial} avBg="#3b82c4" name={meFirst} ts="6:33 PM">
<span className="hiw-men">@OpenWorker</span> summarize this thread
</Msg>
<div className="hiw-cnt">1 reply</div>
<Msg av="OW" avBg="#4a154b" name="OpenWorker" app ts="6:34 PM">
Launch traction: signups up 3.4× since the post
</Msg>
</div>
<div className="hiw-treply">Reply</div>
</div>
</div>
</SlackWin>
<OwWin>
<OwRail hot="Summarize #launch-room" hotSub="via Slack · now" glow />
<div className="hiw-owmain">
<div className="hiw-owtitle hiw-k" style={d("2.6s")}>
Summarize #launch-room <span className="hiw-via">via Slack</span>
</div>
<div className="hiw-owchat">
<div className="hiw-bub user hiw-k" style={d("2.8s")}>@OpenWorker summarize this thread</div>
<div className="hiw-bub agent hiw-k" style={d("3.6s")}>
Reading the thread signups up 3.4×, top referrer is the press page. <i>(replying in the Slack thread)</i>
</div>
</div>
<div className="hiw-owcomposer">Message OpenWorker</div>
</div>
</OwWin>
</>
);
}
/* ---- scene 2: mention INSIDE the open thread panel → the same session ---- */
function SceneThread({ meFirst, meInitial }: { meFirst: string; meInitial: string }) {
return (
<>
<span className="hiw-spark" style={d("1.9s")} />
<Sticky d="3.2s" r pos={{ left: "52%", top: "10%" }}>chatting in the thread continues the SAME conversation </Sticky>
<SlackWin>
<SlackRail active="launch-room" />
<div className="hiw-slmain">
<div className="hiw-slhead"># launch-room <span className="hiw-sub">· 24 members</span></div>
<div className="hiw-slmsgs">
<SlackDate label="Today" />
<Msg av="P" avBg="#7c6cd0" name="Priya N" ts="6:31 PM">
signups are spiking since the post 📈
</Msg>
<Msg
av={meInitial} avBg="#3b82c4" name={meFirst} ts="6:33 PM"
extra={
<span className="hiw-replybar">
<span className="hiw-sav2">OW</span> 2 replies
<span className="hiw-later">Today at 6:36 PM</span>
</span>
}
>
<span className="hiw-men">@OpenWorker</span> summarize this thread
</Msg>
</div>
<SlackComposer placeholder="Message #launch-room" />
{/* thread panel open from the start — the new mentions play INSIDE it */}
<div className="hiw-slthread">
<div className="hiw-th">Thread <span className="hiw-sub"># launch-room</span><span className="hiw-x"></span></div>
<div className="hiw-tmsgs">
<Msg av={meInitial} avBg="#3b82c4" name={meFirst} ts="6:33 PM">
<span className="hiw-men">@OpenWorker</span> summarize this thread
</Msg>
<div className="hiw-cnt">2 replies</div>
<Msg av="OW" avBg="#4a154b" name="OpenWorker" app ts="6:34 PM">
Launch traction: signups up 3.4×
</Msg>
<Msg av="P" avBg="#7c6cd0" name="Priya N" ts="6:36 PM" delay=".8s">
<span className="hiw-men">@OpenWorker</span> break it down by country?
</Msg>
<Msg av="OW" avBg="#4a154b" name="OpenWorker" app ts="6:36 PM" delay="4.8s">
Top: US 41% · India 22% · Germany 9%
</Msg>
</div>
<div className="hiw-treply">Reply</div>
</div>
</div>
</SlackWin>
<OwWin>
<div className="hiw-owrail">
<div className="hiw-brand">OpenWorker</div>
<div className="hiw-newbtn"> New session</div>
<div className="hiw-ownav"> Search</div>
<div className="hiw-ownav"> Automations</div>
<div className="hiw-sect">RECENT</div>
<div className="hiw-sess hot hiw-stay hiw-glow" style={{ "--g": "2.4s" } as React.CSSProperties}>
<b>Summarize #launch-room</b>via Slack
</div>
<div className="hiw-sess"><b>Jira vs Linear</b>Coworker</div>
</div>
<div className="hiw-owmain">
<div className="hiw-owtitle">
Summarize #launch-room <span className="hiw-via">via Slack same session</span>
</div>
<div className="hiw-owchat">
<div className="hiw-bub agent hiw-stay">signups up 3.4×, top referrer is the press page.</div>
<div className="hiw-bub user hiw-k" style={d("2.6s")}>break it down by country?</div>
<div className="hiw-bub agent hiw-k" style={d("3.8s")}>
Top countries: US 41%, India 22%, Germany 9% context kept from the whole thread.
</div>
</div>
<div className="hiw-owcomposer">Message OpenWorker</div>
</div>
</OwWin>
</>
);
}
/* ---- scene 3: a teammate's first mention waits for your OK ---- */
function SceneTeammates() {
return (
<>
<span className="hiw-spark" style={d("1.9s")} />
<Sticky d="3.4s" pos={{ left: "53%", bottom: "10%" }}>first-time senders wait for your OK</Sticky>
<SlackWin>
<SlackRail active="launch-room" />
<div className="hiw-slmain">
<div className="hiw-slhead"># launch-room <span className="hiw-sub">· 24 members</span></div>
<div className="hiw-slmsgs">
<SlackDate label="Today" />
<Msg
av="P" avBg="#7c6cd0" name="Priya N" ts="6:41 PM" delay=".7s"
extra={
<span className="hiw-replybar hiw-k" style={d("5.6s")}>
<span className="hiw-sav2">OW</span> 1 reply
<span className="hiw-later">after you allow</span>
</span>
}
>
<span className="hiw-men">@OpenWorker</span> pull the signup numbers?
</Msg>
</div>
<SlackComposer placeholder="Message #launch-room" />
</div>
</SlackWin>
<OwWin>
<OwRail hot="Summarize #launch-room" hotSub="via Slack" />
<div className="hiw-owmain">
<div className="hiw-owtitle">Slack {WS_NAME}</div>
<div className="hiw-waitrow hiw-k hiw-glow" style={d("2s", { "--g": "2.5s" })}>
<span className="min-w-0"><b>Priya N</b> is waiting</span>
<span className="hiw-allowbtn ml-auto">Allow &amp; deliver</span>
</div>
<div className="hiw-waitcap hiw-k" style={d("3.4s")}>
Each teammate&apos;s <b>first</b> mention waits for your OK then they&apos;re on the People list and it flows.
</div>
</div>
</OwWin>
</>
);
}
+184
View File
@@ -1355,6 +1355,190 @@ html[data-theme="dark"] :not(.connector-badge) > .connector-icon[data-dark-mark]
.art-chip-meta span { font-size: 11px; color: var(--faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.art-chip-open { margin-left: 8px; flex: none; font-size: 11.5px; color: var(--accent); font-weight: 500; }
/* ---- UX-027: Slack post-connect "how mentions reach you" card ----
Split-scene carousel: the Slack window is PINNED to modern light-Slack (aubergine
chrome, white pill for the active channel, purple top toolbar with the search
field it should read as a screenshot of Slack, not our UI). The OpenWorker
window uses app tokens. Everything enters via .hiw-k + an inline --d delay once
.hiw-play is set. */
.hiw-tab { position: relative; font: inherit; font-size: 12px; color: var(--muted); background: none;
border: 0; padding: 5px 9px 7px; cursor: pointer; border-radius: 7px 7px 0 0; }
.hiw-tab:hover { color: var(--ink); background: var(--paper); }
.hiw-tab.on { color: var(--ink); font-weight: 600; }
.hiw-prog { position: absolute; left: 8px; right: 8px; bottom: -1px; height: 2px;
background: var(--line-strong); opacity: 0; }
.hiw-tab.on .hiw-prog { opacity: 1; }
.hiw-tab.on .hiw-prog i { display: block; height: 100%; width: 0; background: var(--accent);
animation: hiw-prog-fill var(--hiw-dur, 8s) linear forwards; }
@keyframes hiw-prog-fill { to { width: 100%; } }
.hiw-scene { position: relative; display: flex; gap: 16px; }
.hiw-win { border: 1px solid var(--line-strong); border-radius: 10px; overflow: hidden;
display: flex; flex-direction: column; height: 252px; box-shadow: 0 5px 14px rgba(0,0,0,.08); }
/* ===== Slack side — modern light Slack, fixed palette ===== */
.hiw-sl { flex: 1.3 1 0; min-width: 0; background: #fff; color: #1d1c1d; }
/* window top = Slack's aubergine toolbar: traffic lights + the search pill */
.hiw-sltop { display: flex; align-items: center; gap: 7px; padding: 4px 9px; background: #350d36; }
.hiw-sltop .hiw-dots { display: flex; gap: 3.5px; flex: none; }
.hiw-sltop .hiw-dots i { width: 6px; height: 6px; border-radius: 50%; }
.hiw-sltop .hiw-dots i:nth-child(1) { background: #ff5f57; }
.hiw-sltop .hiw-dots i:nth-child(2) { background: #febc2e; }
.hiw-sltop .hiw-dots i:nth-child(3) { background: #28c840; }
.hiw-slsearch { flex: 1; min-width: 0; display: flex; align-items: center; gap: 4px;
background: rgba(255,255,255,.24); border-radius: 5px; padding: 1.5px 7px;
font-size: 8px; color: #f4ecf4; white-space: nowrap; overflow: hidden; }
.hiw-slbody { display: flex; flex: 1; min-height: 0; position: relative; }
.hiw-slrail { width: 92px; flex: none; background: #4a154b; color: #d5c4d5; padding: 5px 0 5px; font-size: 9px; }
.hiw-slrail .hiw-ws { color: #fff; font-weight: 800; font-size: 10px; padding: 1px 8px 5px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.hiw-slnav { display: flex; align-items: center; gap: 4px; padding: 1.5px 8px;
white-space: nowrap; overflow: hidden; opacity: .92; }
.hiw-ic { width: 9px; height: 9px; flex: none; stroke: currentColor; fill: none;
stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.hiw-appav { width: 10px; height: 10px; border-radius: 3px; background: #fff; color: #4a154b;
display: grid; place-items: center; font-size: 6px; font-weight: 800; flex: none; }
.hiw-slrail .hiw-sect { padding: 6px 8px 2px; font-size: 8.5px; opacity: .72; }
.hiw-slrail .hiw-ch { margin: 0 4px; padding: 1.5px 5px; border-radius: 5px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* modern Slack: the ACTIVE channel is a white pill with aubergine text */
.hiw-slrail .hiw-ch.on { background: #fff; color: #2b0f2c; }
.hiw-slrail .hiw-pres { display: inline-block; width: 5px; height: 5px; border-radius: 50%;
background: #2bac76; margin-right: 3px; vertical-align: 1px; }
.hiw-slmain { flex: 1; min-width: 0; display: flex; flex-direction: column; background: #fff; }
.hiw-slhead { display: flex; align-items: baseline; gap: 5px; padding: 5px 9px 4px; border-bottom: 1px solid #e3e2e3;
font-size: 10px; font-weight: 800; }
.hiw-slhead .hiw-sub { font-weight: 400; color: #868686; font-size: 8.5px; }
.hiw-slmsgs { flex: 1; padding: 5px 9px 4px; display: flex; flex-direction: column; gap: 6px; overflow: hidden; }
/* the centered date-divider pill */
.hiw-sldate { display: flex; align-items: center; gap: 6px; margin: 1px 0; }
.hiw-sldate::before, .hiw-sldate::after { content: ""; flex: 1; border-top: 1px solid #e3e2e3; }
.hiw-sldate span { font-size: 7.5px; font-weight: 700; color: #454245; border: 1px solid #e3e2e3;
border-radius: 999px; padding: 1px 7px; background: #fff; }
.hiw-slm { display: flex; gap: 6px; font-size: 9.5px; line-height: 1.35; }
.hiw-sav { width: 20px; height: 20px; border-radius: 5px; flex: none; display: grid; place-items: center;
font-size: 8px; font-weight: 800; color: #fff; }
.hiw-nm { font-weight: 800; color: #1d1c1d; }
.hiw-ts { font-size: 8px; color: #868686; margin-left: 3px; font-weight: 400; }
.hiw-appb { font-size: 6.5px; font-weight: 700; color: #868686; background: #efefef; border-radius: 3px;
padding: 0.5px 3px; margin-left: 3px; vertical-align: 1px; letter-spacing: .03em; }
.hiw-men { color: #1264a3; background: #e8f5fa; border-radius: 3px; padding: 0 3px; font-weight: 600;
white-space: nowrap; }
.hiw-replybar { display: inline-flex; align-items: center; gap: 4px; margin-top: 2px; font-size: 8.5px;
color: #1264a3; font-weight: 700; }
.hiw-replybar .hiw-sav2 { width: 13px; height: 13px; border-radius: 3px; background: #4a154b; color: #fff;
display: grid; place-items: center; font-size: 5.5px; font-weight: 800; }
.hiw-replybar .hiw-later { color: #868686; font-weight: 400; }
/* two-row Slack composer: formatting toolbar on top, then + / input / send */
.hiw-slcomposer { margin: 0 9px 7px; border: 1px solid #8d8d8daa; border-radius: 7px; overflow: hidden; flex: none; }
.hiw-slctools { display: flex; align-items: center; gap: 7px; padding: 2.5px 8px 2px;
font-size: 8px; color: #868686; background: #f8f8f8; }
.hiw-slctools b { font-weight: 800; } .hiw-slctools i { font-style: italic; }
.hiw-slctools u { text-decoration: underline; } .hiw-slctools s { text-decoration: line-through; }
.hiw-slcrow { display: flex; align-items: center; gap: 5px; padding: 3px 7px 4px; font-size: 9px; color: #868686; }
.hiw-slcrow .hiw-plus { width: 13px; height: 13px; border-radius: 50%; background: #efefef; color: #555;
display: grid; place-items: center; font-size: 9px; flex: none; }
.hiw-slcrow .hiw-send { margin-left: auto; width: 16px; height: 13px; border-radius: 3px; background: #eaeaea;
color: #a0a0a0; display: grid; place-items: center; font-size: 7px; flex: none; }
/* thread panel (slides in like the real thread rail) */
.hiw-slthread { position: absolute; top: 0; right: 0; bottom: 0; width: 60%; background: #fff;
border-left: 1px solid #e3e2e3; box-shadow: -7px 0 15px rgba(0,0,0,.08);
display: flex; flex-direction: column; z-index: 2; }
.hiw-slthread .hiw-th { display: flex; align-items: baseline; gap: 5px; padding: 5px 9px 4px;
border-bottom: 1px solid #e3e2e3; font-size: 10px; font-weight: 800; }
.hiw-slthread .hiw-x { margin-left: auto; color: #868686; font-weight: 400; }
.hiw-slthread .hiw-tmsgs { flex: 1; min-height: 0; padding: 6px 9px; display: flex; flex-direction: column;
gap: 6px; overflow: hidden; }
/* "N replies ————" rule, like Slack's reply-count divider */
.hiw-slthread .hiw-cnt { display: flex; align-items: center; gap: 5px; font-size: 8px; color: #868686; }
.hiw-slthread .hiw-cnt::after { content: ""; flex: 1; border-top: 1px solid #e3e2e3; }
.hiw-slthread .hiw-treply { margin: 0 8px 7px; border: 1px solid #8d8d8daa; border-radius: 7px;
padding: 3px 7px; font-size: 8.5px; color: #868686; flex: none; }
/* ===== OpenWorker side — app tokens ===== */
.hiw-ow { flex: 1 1 0; min-width: 0; background: var(--paper); }
.hiw-owtop { display: flex; align-items: center; gap: 6px; padding: 4px 9px; font-size: 9.5px; font-weight: 600;
background: var(--panel); color: var(--muted); border-bottom: 1px solid var(--line); }
.hiw-owtop .hiw-dots { display: flex; gap: 3.5px; }
.hiw-owtop .hiw-dots i { width: 6px; height: 6px; border-radius: 50%; background: var(--line-strong); }
.hiw-owbody { display: flex; flex: 1; min-height: 0; }
.hiw-owrail { width: 96px; flex: none; background: var(--panel); border-right: 1px solid var(--line); padding: 6px 5px; }
.hiw-owrail .hiw-brand { font-weight: 700; font-size: 9px; letter-spacing: -.015em; padding: 0 3px 5px; color: var(--ink); }
.hiw-newbtn { background: var(--accent); color: #fff; border-radius: 5px; padding: 3px 6px; font-size: 8px;
font-weight: 600; margin-bottom: 5px; }
.hiw-ownav { display: flex; align-items: center; gap: 4px; color: var(--muted); font-size: 8px; padding: 2px 3px; }
.hiw-owrail .hiw-sect { font-size: 7px; letter-spacing: .07em; font-weight: 700; color: var(--faint); margin: 6px 3px 2px; }
.hiw-sess { font-size: 8px; padding: 3px 4px; border-radius: 4px; color: var(--faint); line-height: 1.25; }
.hiw-sess b { display: block; color: var(--ink); font-weight: 600; font-size: 8px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.hiw-sess.hot { background: var(--accent-soft); }
.hiw-owmain { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.hiw-owtitle { padding: 4px 8px; font-size: 8.5px; font-weight: 600; border-bottom: 1px solid var(--line);
color: var(--ink); background: var(--panel); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.hiw-owtitle .hiw-via { font-weight: 400; color: var(--faint); font-size: 8px; }
.hiw-owchat { flex: 1; padding: 7px 8px; display: flex; flex-direction: column; gap: 5px; overflow: hidden; }
.hiw-bub { font-size: 8.5px; line-height: 1.4; border-radius: 7px; padding: 4px 7px; max-width: 94%; }
.hiw-bub.user { background: var(--accent-soft); align-self: flex-end; color: var(--ink); }
.hiw-bub.agent { background: var(--panel); border: 1px solid var(--line); align-self: flex-start; color: var(--muted); }
.hiw-owcomposer { margin: 0 8px 7px; border: 1px solid var(--line-strong); border-radius: 6px; padding: 4px 7px;
font-size: 8.5px; color: var(--faint); background: var(--panel); flex: none; }
.hiw-waitrow { display: flex; align-items: center; gap: 6px; font-size: 8.5px; padding: 5px 7px; margin: 9px 8px 0;
border: 1px solid var(--line); border-radius: 7px; background: var(--warn-soft); color: var(--ink); }
.hiw-allowbtn { font-size: 8.5px; font-weight: 600; color: #fff; background: var(--accent); border: 0;
border-radius: 999px; padding: 2px 8px; white-space: nowrap; flex: none; }
.hiw-waitcap { margin: 5px 10px; font-size: 8.5px; color: var(--muted); }
/* the travelling spark between windows */
.hiw-spark { position: absolute; top: 46%; width: 8px; height: 8px; border-radius: 50%;
background: var(--accent); box-shadow: 0 0 0 4px var(--accent-soft); opacity: 0; z-index: 4; }
.hiw-play .hiw-spark { animation: hiw-shoot .9s ease-in-out forwards; animation-delay: var(--d, 0s); }
@keyframes hiw-shoot {
0% { opacity: 0; left: 50%; } 15% { opacity: 1; }
85% { opacity: 1; } 100% { opacity: 0; left: 57%; }
}
/* post-it notes (rev 7): the concept in five hand-written words, slapped onto the
scene at its beat; they fade near the loop's end so they read as annotation. */
.hiw-sticky {
position: absolute; width: 118px; padding: 8px 10px 10px; z-index: 5;
background: #fde68a; color: #6b4d11;
font: 10.5px/1.35 "Marker Felt", "Bradley Hand", "Segoe Print", "Comic Sans MS", cursive;
box-shadow: 2px 4px 10px rgba(0,0,0,.22); transform: rotate(-3.5deg);
}
.hiw-sticky.r { transform: rotate(2.5deg); }
.hiw-sticky::before { /* the bit of tape */
content: ""; position: absolute; top: -5px; left: 50%; width: 30px; height: 9px;
transform: translateX(-50%) rotate(-1deg); background: rgba(255,255,255,.55);
box-shadow: 0 1px 2px rgba(0,0,0,.08);
}
html[data-theme="dark"] .hiw-sticky { background: #eab308; color: #3d2c05; }
.hiw-play .hiw-k.hiw-sticky {
animation: hiw-slap .45s ease forwards, hiw-sticky-out .6s ease forwards 7.2s;
animation-delay: var(--d, 0s), 7.2s;
}
.hiw-play .hiw-k.hiw-sticky.r {
animation-name: hiw-slap-r, hiw-sticky-out;
}
@keyframes hiw-slap { from { opacity: 0; transform: rotate(-3.5deg) scale(1.18); } to { opacity: 1; transform: rotate(-3.5deg) scale(1); } }
@keyframes hiw-slap-r { from { opacity: 0; transform: rotate(2.5deg) scale(1.18); } to { opacity: 1; transform: rotate(2.5deg) scale(1); } }
@keyframes hiw-sticky-out { to { opacity: 0; } }
/* animation primitives */
.hiw-k { opacity: 0; }
.hiw-play .hiw-k { animation: hiw-fade-up .45s ease forwards; animation-delay: var(--d, 0s); }
@keyframes hiw-fade-up { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: none; } }
.hiw-play .hiw-glow { animation: hiw-fade-up .45s ease forwards, hiw-glow 1.2s ease var(--g, 1s) 2;
animation-delay: var(--d, 0s), var(--g, 1s); }
.hiw-play .hiw-stay.hiw-glow { opacity: 1; animation: hiw-glow 1.2s ease var(--g, 1s) 2; }
@keyframes hiw-glow { 0%,100% { box-shadow: 0 0 0 0 transparent; } 50% { box-shadow: 0 0 0 3px var(--accent-soft); } }
.hiw-stay { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
.hiw-k, .hiw-play .hiw-k, .hiw-play .hiw-glow, .hiw-play .hiw-spark { animation: none !important; opacity: 1 !important; }
.hiw-play .hiw-spark { opacity: 0 !important; }
.hiw-tab.on .hiw-prog i { animation: none !important; width: 100%; }
}
/* ---- UX-026: automation-start toast (top-right, 5s; schedule-fired runs only) ---- */
.toast-pulse { animation: toast-pulse 1.2s ease infinite; }
@keyframes toast-pulse { 50% { opacity: .35; } }