@alexkroman1/aai-ui 13.3.0 → 14.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +159 -69
- package/dist/{_colors-CZ6OlPbL.js → _colors-CpZO-88A.js} +24 -1
- package/dist/_recover-run.d.ts +2 -2
- package/dist/_submission-state.d.ts +92 -0
- package/dist/_upload-files.d.ts +2 -2
- package/dist/_upload-report.d.ts +26 -0
- package/dist/{_utils-CyzjK0gW.js → _utils-DnQDM9Uy.js} +3 -3
- package/dist/_utils.d.ts +3 -3
- package/dist/_web-storage.d.ts +43 -0
- package/dist/_workflow-files.d.ts +1 -1
- package/dist/agent-state-labels.d.ts +60 -0
- package/dist/audio.js +20 -14
- package/dist/{chat-view-Bv5VFJIE.js → chat-view-C_3T7Ln8.js} +96 -29
- package/dist/{client-config-DD820zHn.js → client-config-DJQHnYjm.js} +4 -4
- package/dist/client-config.d.ts +4 -4
- package/dist/client-dir.d.ts +1 -1
- package/dist/client-dir.js +1 -1
- package/dist/components/_colors.d.ts +23 -0
- package/dist/components/_form-readiness.d.ts +1 -1
- package/dist/components/bullet-list.d.ts +74 -0
- package/dist/components/button.js +3 -3
- package/dist/components/chat-view.js +1 -1
- package/dist/components/console-shell.d.ts +16 -20
- package/dist/components/controls.js +3 -3
- package/dist/components/facts.d.ts +81 -0
- package/dist/components/form-fields.d.ts +6 -6
- package/dist/components/form-types.d.ts +1 -1
- package/dist/components/form.d.ts +1 -1
- package/dist/components/message-list.js +1 -1
- package/dist/components/session-error-banner.d.ts +69 -0
- package/dist/components/start-screen.js +1 -1
- package/dist/components/tool-call-block.js +1 -1
- package/dist/components/tool-config-context.d.ts +1 -1
- package/dist/components/workflow-progress.d.ts +11 -4
- package/dist/context.d.ts +142 -19
- package/dist/context.js +155 -17
- package/dist/default-client/assets/{audio-BuDICbPf.js → audio-9zQsNc1w.js} +1 -1
- package/dist/default-client/assets/index-BTv30Z4F.css +2 -0
- package/dist/default-client/assets/index-RAZ-29Sz.js +284 -0
- package/dist/default-client/index.html +2 -2
- package/dist/define-client.d.ts +19 -19
- package/dist/define-client.js +19 -19
- package/dist/hooks.d.ts +44 -8
- package/dist/hooks.js +19 -13
- package/dist/index.d.ts +11 -7
- package/dist/index.js +418 -185
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +5 -5
- package/dist/{message-list-C0pL7x41.js → message-list-CdOnSh5m.js} +19 -12
- package/dist/page.d.ts +11 -11
- package/dist/session-core-audio-setup.d.ts +1 -1
- package/dist/session-core-dial.d.ts +0 -2
- package/dist/{session-core-C9elBIdu.js → session-core-gwePM95B.js} +125 -52
- package/dist/session-core-messages.d.ts +2 -2
- package/dist/session-core-types.d.ts +58 -1
- package/dist/session-core.d.ts +6 -6
- package/dist/session-core.js +2 -2
- package/dist/session-resume-store.d.ts +3 -3
- package/dist/{tool-call-block-Bunc6rCw.js → tool-call-block-C2t_5fpp.js} +27 -9
- package/dist/{tool-config-context-Bh8p3DtG.js → tool-config-context-Es4YUzV2.js} +1 -1
- package/dist/types.d.ts +19 -4
- package/dist/types.js +2 -2
- package/dist/{url-chips-C2u7QPv8.js → url-chips-BxhzZgk2.js} +4 -4
- package/dist/use-conversation.d.ts +1 -1
- package/dist/{use-user-transcript-DFTSEuZN.js → use-user-transcript-uyHhzy4d.js} +3 -2
- package/dist/use-workflow-form.d.ts +34 -2
- package/dist/{use-workflow-run-CXGEcM0l.js → use-workflow-run-CP2ekKPV.js} +3 -6
- package/dist/use-workflow-stream.d.ts +1 -1
- package/dist/workflow-client.d.ts +1 -1
- package/package.json +2 -2
- package/styles.css +78 -0
- package/dist/default-client/assets/index-B1_ROnTJ.js +0 -284
- package/dist/default-client/assets/index-S5fkKi6B.css +0 -2
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useTheme } from "./context.js";
|
|
2
|
-
import {
|
|
2
|
+
import { a as inkTint } from "./_colors-CpZO-88A.js";
|
|
3
3
|
import { t as Eyebrow } from "./eyebrow-UfmSz9yy.js";
|
|
4
|
-
import { i as tryParseJSON, r as truncate } from "./_utils-
|
|
5
|
-
import { n as useToolConfig } from "./tool-config-context-
|
|
4
|
+
import { i as tryParseJSON, r as truncate } from "./_utils-DnQDM9Uy.js";
|
|
5
|
+
import { n as useToolConfig } from "./tool-config-context-Es4YUzV2.js";
|
|
6
6
|
import clsx from "clsx";
|
|
7
7
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
import { memo, useMemo, useState } from "react";
|
|
@@ -123,6 +123,29 @@ function formatResult(result) {
|
|
|
123
123
|
return parsed === result ? result : JSON.stringify(parsed, null, 2);
|
|
124
124
|
}
|
|
125
125
|
/**
|
|
126
|
+
* The expanded result pane.
|
|
127
|
+
*
|
|
128
|
+
* Its own component so the parse-and-pretty-print happens only when the row is
|
|
129
|
+
* actually OPEN. `ToolCallRow` renders `children` under `isOpen && canExpand`,
|
|
130
|
+
* and a row starts closed — so computing this in the parent meant every row
|
|
131
|
+
* paid `JSON.parse` + `JSON.stringify(_, null, 2)` over its whole result, and
|
|
132
|
+
* then RETAINED the pretty string, for a panel nobody clicked. The burst case
|
|
133
|
+
* is `history.restored`, which mounts up to `DEFAULT_MAX_HISTORY` rows in one
|
|
134
|
+
* commit.
|
|
135
|
+
*
|
|
136
|
+
* Creating this element is free; React only runs the body once it is mounted.
|
|
137
|
+
*/
|
|
138
|
+
function ToolCallResult({ result }) {
|
|
139
|
+
const theme = useTheme();
|
|
140
|
+
const formatted = useMemo(() => formatResult(result), [result]);
|
|
141
|
+
if (!formatted) return null;
|
|
142
|
+
return /* @__PURE__ */ jsx("pre", {
|
|
143
|
+
className: "font-aai-mono text-xs px-3.5 py-3 m-0 whitespace-pre-wrap wrap-break-word",
|
|
144
|
+
style: { color: inkTint(theme.text, theme.surface, 75) },
|
|
145
|
+
children: formatted
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
126
149
|
* Renders a tool invocation as the design system's console row (see
|
|
127
150
|
* `ToolCallRow`): a small outlined "TOOL" chip (or the tool's configured
|
|
128
151
|
* icon), the tool name in mono, a truncated args preview, and a rotating
|
|
@@ -151,7 +174,6 @@ const ToolCallBlock = memo(function ToolCallBlock({ toolCall, className }) {
|
|
|
151
174
|
const title = config?.label || toolCall.name;
|
|
152
175
|
const icon = config?.icon;
|
|
153
176
|
const canExpand = !isPending && Boolean(toolCall.result);
|
|
154
|
-
const formatted = useMemo(() => toolCall.result ? formatResult(toolCall.result) : "", [toolCall.result]);
|
|
155
177
|
const subtitle = useMemo(() => {
|
|
156
178
|
const args = toolCall.args;
|
|
157
179
|
if (toolCall.name === "run_code" && args.code) return truncate(String(args.code).split("\n")[0] ?? "");
|
|
@@ -175,11 +197,7 @@ const ToolCallBlock = memo(function ToolCallBlock({ toolCall, className }) {
|
|
|
175
197
|
borderColor: theme.border
|
|
176
198
|
},
|
|
177
199
|
children: String(toolCall.args.code)
|
|
178
|
-
}),
|
|
179
|
-
className: "font-aai-mono text-xs px-3.5 py-3 m-0 whitespace-pre-wrap wrap-break-word",
|
|
180
|
-
style: { color: inkTint(theme.text, theme.surface, 75) },
|
|
181
|
-
children: formatted
|
|
182
|
-
})] }) : void 0
|
|
200
|
+
}), toolCall.result && /* @__PURE__ */ jsx(ToolCallResult, { result: toolCall.result })] }) : void 0
|
|
183
201
|
});
|
|
184
202
|
});
|
|
185
203
|
//#endregion
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createContext, useContext } from "react";
|
|
2
2
|
//#region src/components/tool-config-context.ts
|
|
3
3
|
/**
|
|
4
|
-
* Context for tool display configuration. Installed by `
|
|
4
|
+
* Context for tool display configuration. Installed by `mountClient()` from
|
|
5
5
|
* `ClientConfig.tools`; the built-in components read it via `useToolConfig`.
|
|
6
6
|
*
|
|
7
7
|
* @internal
|
package/dist/types.d.ts
CHANGED
|
@@ -23,8 +23,8 @@ export { CAPTURE_STOP_ACK_TIMEOUT_MS, CLIENT_AUDIO_LEAD_MS, HEARD_AUDIO_LAG_MS,
|
|
|
23
23
|
*
|
|
24
24
|
* On `@alexkroman1/aai-ui/internal` rather than the root barrel, for the same
|
|
25
25
|
* reason as the audio budgets above: it is a framework decision with no
|
|
26
|
-
* `
|
|
27
|
-
* chrome that bypasses `
|
|
26
|
+
* `mountClient()` field to set, and the root is the authoring surface. A custom
|
|
27
|
+
* chrome that bypasses `mountClient()` and opens its own microphone reaches it there
|
|
28
28
|
* alongside the providers it also needs.
|
|
29
29
|
*/
|
|
30
30
|
export declare const VOICE_CAPTURE_CONSTRAINTS: MediaTrackConstraints;
|
|
@@ -135,11 +135,26 @@ export type SessionError = {
|
|
|
135
135
|
readonly code: SessionErrorCode;
|
|
136
136
|
/** A human-readable description of the error. */
|
|
137
137
|
readonly message: string;
|
|
138
|
+
/**
|
|
139
|
+
* Whether the session is OVER.
|
|
140
|
+
*
|
|
141
|
+
* `false` means surface the message and keep the session interactive — a
|
|
142
|
+
* turn-level failure over a server that kept running. `true` means the call
|
|
143
|
+
* is dead and the microphone has been released.
|
|
144
|
+
*
|
|
145
|
+
* Required rather than optional, because the wire always carries it
|
|
146
|
+
* (`error.reported` declares `fatal: z.boolean()`) and a client that cannot
|
|
147
|
+
* tell the two apart has to guess which banner to render. It was dropped one
|
|
148
|
+
* line before reaching here for long enough that this type's own doc, and
|
|
149
|
+
* the reference page generated from it, described a field that did not
|
|
150
|
+
* exist.
|
|
151
|
+
*/
|
|
152
|
+
readonly fatal: boolean;
|
|
138
153
|
};
|
|
139
154
|
/**
|
|
140
155
|
* Options for creating a voice session — the shared field set accepted by
|
|
141
|
-
* both `
|
|
142
|
-
* defaults `platformUrl` from `location.href`, while `
|
|
156
|
+
* both `mountClient()` and `createBrowserSession`. The one difference: `mountClient()`
|
|
157
|
+
* defaults `platformUrl` from `location.href`, while `createBrowserSession`
|
|
143
158
|
* requires it.
|
|
144
159
|
*
|
|
145
160
|
* @public
|
package/dist/types.js
CHANGED
|
@@ -22,8 +22,8 @@ import { CAPTURE_STOP_ACK_TIMEOUT_MS, CLIENT_AUDIO_LEAD_MS, HEARD_AUDIO_LAG_MS,
|
|
|
22
22
|
*
|
|
23
23
|
* On `@alexkroman1/aai-ui/internal` rather than the root barrel, for the same
|
|
24
24
|
* reason as the audio budgets above: it is a framework decision with no
|
|
25
|
-
* `
|
|
26
|
-
* chrome that bypasses `
|
|
25
|
+
* `mountClient()` field to set, and the root is the authoring surface. A custom
|
|
26
|
+
* chrome that bypasses `mountClient()` and opens its own microphone reaches it there
|
|
27
27
|
* alongside the providers it also needs.
|
|
28
28
|
*/
|
|
29
29
|
const VOICE_CAPTURE_CONSTRAINTS = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useSessionSelector, useTheme } from "./context.js";
|
|
2
|
-
import {
|
|
3
|
-
import { t as pageBaseUrl } from "./_utils-
|
|
2
|
+
import { a as inkTint, i as focusRingStyle, n as FOCUS_RING } from "./_colors-CpZO-88A.js";
|
|
3
|
+
import { t as pageBaseUrl } from "./_utils-DnQDM9Uy.js";
|
|
4
4
|
import clsx from "clsx";
|
|
5
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
import { useEffect, useRef, useState } from "react";
|
|
@@ -32,12 +32,12 @@ function UrlChip({ label, url, hint, testId, className }) {
|
|
|
32
32
|
type: "button",
|
|
33
33
|
onClick: copy,
|
|
34
34
|
title: `${hint} (click to copy)\n${url}`,
|
|
35
|
-
className: clsx("flex items-center gap-1.5 min-w-0 appearance-none m-0 px-2 py-1 rounded-aai border cursor-pointer text-[11px] leading-none font-aai-mono",
|
|
35
|
+
className: clsx("flex items-center gap-1.5 min-w-0 appearance-none m-0 px-2 py-1 rounded-aai border cursor-pointer text-[11px] leading-none font-aai-mono", FOCUS_RING, className),
|
|
36
36
|
style: {
|
|
37
37
|
background: inkTint(theme.text, theme.surface, 3),
|
|
38
38
|
borderColor: theme.border,
|
|
39
39
|
color: inkTint(theme.text, theme.surface, 65),
|
|
40
|
-
|
|
40
|
+
...focusRingStyle(theme.primary)
|
|
41
41
|
},
|
|
42
42
|
"data-testid": testId,
|
|
43
43
|
children: [/* @__PURE__ */ jsx("span", {
|
|
@@ -88,7 +88,7 @@ export type UseConversationResult = {
|
|
|
88
88
|
* Subscribe to the conversation: the interleaved exchange, the streaming
|
|
89
89
|
* utterance, the live transcript and the thinking rule — with no markup.
|
|
90
90
|
*
|
|
91
|
-
* Must be used inside the provider `
|
|
91
|
+
* Must be used inside the provider `mountClient()` installs.
|
|
92
92
|
*
|
|
93
93
|
* @example A custom bubble, keeping every rule `<MessageList>` knows
|
|
94
94
|
* ```tsx
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useSessionSelector } from "./context.js";
|
|
2
|
+
import { useMemo } from "react";
|
|
2
3
|
//#region src/use-user-transcript.ts
|
|
3
4
|
/**
|
|
4
5
|
* `useUserTranscript` — what the caller is saying RIGHT NOW, read correctly.
|
|
@@ -57,11 +58,11 @@ const TRANSCRIBING_PLACEHOLDER = "…";
|
|
|
57
58
|
*/
|
|
58
59
|
function useUserTranscript() {
|
|
59
60
|
const partial = useSessionSelector((snapshot) => snapshot.userTranscript);
|
|
60
|
-
return {
|
|
61
|
+
return useMemo(() => ({
|
|
61
62
|
speaking: partial !== null,
|
|
62
63
|
text: displayText(partial),
|
|
63
64
|
partial
|
|
64
|
-
};
|
|
65
|
+
}), [partial]);
|
|
65
66
|
}
|
|
66
67
|
/** The three cases, spelled out: silent, detected-but-wordless, and words. */
|
|
67
68
|
function displayText(partial) {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* `wait` here when the page really does want one request, and the run is
|
|
32
32
|
* followed from the same id either way.
|
|
33
33
|
*/
|
|
34
|
-
import type { AnyWorkflowDef,
|
|
34
|
+
import type { AnyWorkflowDef, UploadParallelOption, UploadProgress, WorkflowOutputOf } from "@alexkroman1/aai/workflow-api";
|
|
35
35
|
import type { FormValues } from "./components/form-types.ts";
|
|
36
36
|
import type { WorkflowApi, WorkflowRun } from "./workflow-client.ts";
|
|
37
37
|
import type { SubmitInputOf } from "./workflow-def-types.ts";
|
|
@@ -119,6 +119,38 @@ export type WorkflowSubmission<R = unknown, I = unknown> = {
|
|
|
119
119
|
cancel: () => Promise<boolean>;
|
|
120
120
|
/** The run, once started, followed to completion. */
|
|
121
121
|
run: WorkflowRun<R> | undefined;
|
|
122
|
+
/**
|
|
123
|
+
* True from `submit()` on this mount until `reset()` — did THIS page start
|
|
124
|
+
* the run it is showing?
|
|
125
|
+
*
|
|
126
|
+
* A page needs it to say the right sentence and cannot derive it: a run
|
|
127
|
+
* ADOPTED by the mount-time lookup after a reload looks exactly like one this
|
|
128
|
+
* page started. Six templates kept a `useState(false)` next to this hook, set
|
|
129
|
+
* it in their `onSubmit` and mirrored it in their `onClear` — shadow state
|
|
130
|
+
* for a fact only this hook can know, since it is the thing that decides
|
|
131
|
+
* between `submit()` and the recovery lookup. One of the six grew a fourth
|
|
132
|
+
* branch and had to move the whole note into its own module with its own
|
|
133
|
+
* spec, which is what a seam missing one layer down looks like.
|
|
134
|
+
*
|
|
135
|
+
* The RAW fact rather than a derived "recovered", deliberately: with `run`
|
|
136
|
+
* these are three states, not two, and the third is the one a page most needs
|
|
137
|
+
* to explain. `startedHere` is "you pressed the button"; `!startedHere &&
|
|
138
|
+
* !run` is the mount-time lookup still going; `!startedHere && run` is a run
|
|
139
|
+
* this browser started earlier, now in front of somebody who did not press
|
|
140
|
+
* anything. A boolean meaning only the last of those cannot express the
|
|
141
|
+
* middle one.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```ts
|
|
145
|
+
* declare const submission: import("@alexkroman1/aai-ui").WorkflowSubmission;
|
|
146
|
+
* const note = submission.startedHere
|
|
147
|
+
* ? "You can close this tab or reload it — this page will find the run again."
|
|
148
|
+
* : submission.run === undefined
|
|
149
|
+
* ? "Looking for a run this tab started earlier…"
|
|
150
|
+
* : "Still working on the run this tab started earlier. Reloading is safe.";
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
startedHere: boolean;
|
|
122
154
|
/**
|
|
123
155
|
* True from `submit()` until the run reaches a terminal status.
|
|
124
156
|
*
|
|
@@ -214,7 +246,7 @@ export type UseWorkflowSubmitOptions = {
|
|
|
214
246
|
* the default costs nothing where it would not have paid. See
|
|
215
247
|
* `UploadOptions.parallel`.
|
|
216
248
|
*/
|
|
217
|
-
parallel?:
|
|
249
|
+
parallel?: UploadParallelOption;
|
|
218
250
|
};
|
|
219
251
|
/**
|
|
220
252
|
* Start a workflow from a form, and follow the run it creates.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as pageBaseUrl } from "./_utils-
|
|
1
|
+
import { t as pageBaseUrl } from "./_utils-DnQDM9Uy.js";
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { errorMessage } from "@alexkroman1/aai";
|
|
4
4
|
import { omitUndefined } from "@alexkroman1/aai/utils";
|
|
@@ -10,7 +10,7 @@ import { createWorkflowApiClient, isTerminal, isTerminal as isTerminal$1, readEv
|
|
|
10
10
|
* This is the whole client half of a WORKFLOW APP: an agent whose front door is
|
|
11
11
|
* a form rather than a microphone (`workflowApp()`) starts runs
|
|
12
12
|
* here and watches them for the answer. It deliberately does NOT go through
|
|
13
|
-
* `
|
|
13
|
+
* `BrowserSession` — there is no socket, no audio graph, and no session to resume.
|
|
14
14
|
*
|
|
15
15
|
* **The requests themselves are the SDK's now**
|
|
16
16
|
* (`createWorkflowApiClient`, `@alexkroman1/aai/workflow-api`), and what is left
|
|
@@ -466,11 +466,8 @@ function useWorkflowProgress(runId, opts = {}) {
|
|
|
466
466
|
*/
|
|
467
467
|
function watchRunEvents(getClient, runId, onRun, onSettled, onFallback) {
|
|
468
468
|
const controller = new AbortController();
|
|
469
|
-
let handedOver = false;
|
|
470
469
|
const handOver = () => {
|
|
471
|
-
if (
|
|
472
|
-
handedOver = true;
|
|
473
|
-
onFallback();
|
|
470
|
+
if (!controller.signal.aborted) onFallback();
|
|
474
471
|
};
|
|
475
472
|
/**
|
|
476
473
|
* Consume the stream. Resolves `"settled"` when the run reached a state
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* earlier version of this hook took a `cut` callback and uploaded N parts into a
|
|
26
26
|
* "group" that a separate call had to seal; it worked, and every piece of it was
|
|
27
27
|
* something the caller had to get right. The whole of that is replaced by the store
|
|
28
|
-
* publishing `size` as bytes land — which `
|
|
28
|
+
* publishing `size` as bytes land — which `stepReadUpload` already clamped to — so the
|
|
29
29
|
* run does exactly what it does over a finished file and simply waits for windows to
|
|
30
30
|
* become present.
|
|
31
31
|
*
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* This is the whole client half of a WORKFLOW APP: an agent whose front door is
|
|
5
5
|
* a form rather than a microphone (`workflowApp()`) starts runs
|
|
6
6
|
* here and watches them for the answer. It deliberately does NOT go through
|
|
7
|
-
* `
|
|
7
|
+
* `BrowserSession` — there is no socket, no audio graph, and no session to resume.
|
|
8
8
|
*
|
|
9
9
|
* **The requests themselves are the SDK's now**
|
|
10
10
|
* (`createWorkflowApiClient`, `@alexkroman1/aai/workflow-api`), and what is left
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexkroman1/aai-ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "14.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"use-stick-to-bottom": "^1.1.6",
|
|
47
47
|
"use-sync-external-store": "^1.6.0",
|
|
48
48
|
"xstate": "^5.32.6",
|
|
49
|
-
"@alexkroman1/aai": "
|
|
49
|
+
"@alexkroman1/aai": "14.0.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@tailwindcss/vite": "^4.3.3",
|
package/styles.css
CHANGED
|
@@ -85,3 +85,81 @@
|
|
|
85
85
|
-webkit-text-fill-color: transparent;
|
|
86
86
|
animation: aai-shimmer 2s infinite;
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
/*
|
|
90
|
+
* Reduced motion is honoured HERE, once, for everything this stylesheet reaches.
|
|
91
|
+
*
|
|
92
|
+
* Roughly nineteen INFINITE animations ship in an aai app and, until this block,
|
|
93
|
+
* not one of them asked: `prefers-reduced-motion` appeared zero times in the
|
|
94
|
+
* repository. `aai-pulse` on the console shell's live dot, `aai-bounce` on the
|
|
95
|
+
* thinking dots, `aai-shimmer` on every streaming tool row, Tailwind's
|
|
96
|
+
* `animate-pulse` on an indeterminate upload bar — all of them running for as
|
|
97
|
+
* long as the page is open. For a reader with a vestibular disorder that is not
|
|
98
|
+
* decoration, it is the reason they close the tab.
|
|
99
|
+
*
|
|
100
|
+
* **The universal selector is the leverage point, and it is the only thing that
|
|
101
|
+
* reaches the TEMPLATES.** Every template's `client.tsx` opens with
|
|
102
|
+
* `import "@alexkroman1/aai-ui/styles.css"`, and several then declare keyframes
|
|
103
|
+
* of their own in an inline `<style>` block: `dc-pulse`/`dc-slide-in`
|
|
104
|
+
* (dispatch-center), `rt-pulse`/`rt-slide-in` (retail), `crisisPulse`
|
|
105
|
+
* (solo-rpg), and `ic-flicker`/`ic-scanline`/`ic-pulse` (infocom-adventure,
|
|
106
|
+
* which flickers the whole viewport's opacity and sweeps a scanline across the
|
|
107
|
+
* page — the worst thing in the repo for a motion-sensitive reader). Nothing in
|
|
108
|
+
* this package knows those names, and no per-component fix could: it would have
|
|
109
|
+
* to be written again in every template, by every author who copies one, and
|
|
110
|
+
* the author who forgets leaves no trace in a diff. A universal selector does
|
|
111
|
+
* not need to know the name of the animation it is stopping. That is also what
|
|
112
|
+
* makes this the right FILE rather than merely a convenient one — the published
|
|
113
|
+
* sheet is the single artifact every client already imports, so a template opts
|
|
114
|
+
* in by existing, and one written next year opts in before it is written.
|
|
115
|
+
*
|
|
116
|
+
* Four properties, because motion arrives four ways:
|
|
117
|
+
*
|
|
118
|
+
* - `animation-duration` and `animation-iteration-count` — the keyframe half.
|
|
119
|
+
* The count carries its own weight: cut the duration alone and an `infinite`
|
|
120
|
+
* animation is still infinite, merely very fast, which is worse than what it
|
|
121
|
+
* replaced.
|
|
122
|
+
* - `transition-duration` — the property-change half, which no search for
|
|
123
|
+
* `@keyframes` would ever turn up.
|
|
124
|
+
* - `scroll-behavior` — the CSS half of scrolling: a host page or template
|
|
125
|
+
* declaring `scroll-behavior: smooth`, and any `scrollIntoView()` /
|
|
126
|
+
* `scrollTo()` that left the behaviour to CSS. Note what it does NOT reach.
|
|
127
|
+
* `AutoScroll`'s `resize: "smooth"` default is a requestAnimationFrame SPRING
|
|
128
|
+
* inside `use-stick-to-bottom` (which reads no media query of its own, and
|
|
129
|
+
* sets `scroll-behavior: auto` inline while it writes `scrollTop` anyway), so
|
|
130
|
+
* silencing that one is a change in `components/auto-scroll.tsx` and is still
|
|
131
|
+
* owed. Recording the gap beats letting the next reader assume it is covered.
|
|
132
|
+
*
|
|
133
|
+
* `0.01ms` rather than `0s` on both durations is deliberate: a zero-duration
|
|
134
|
+
* animation or transition fires no `animationend`/`transitionend`, so anything
|
|
135
|
+
* awaiting one — ours or a consumer's — waits forever. Effectively instant, and
|
|
136
|
+
* still an event.
|
|
137
|
+
*
|
|
138
|
+
* `!important`, and no `@layer`, is what lets this outrank the three ways motion
|
|
139
|
+
* is actually declared here: a Tailwind utility (`animate-pulse`), a template's
|
|
140
|
+
* own `<style>` block, and the inline `style={{ animation: … }}` that
|
|
141
|
+
* `ConsoleShell` and `MessageList` write — an author `!important` beats a normal
|
|
142
|
+
* inline declaration, which is the only reason those last two are covered. A
|
|
143
|
+
* reader who asked their OS for less motion outranks all three.
|
|
144
|
+
*
|
|
145
|
+
* Biome's `noImportantStyles` warns on all four declarations and the warnings
|
|
146
|
+
* STAY. The rule's argument — that `!important` reverses the cascade — is the
|
|
147
|
+
* property being used on purpose here, this block existing precisely to outrank
|
|
148
|
+
* a declaration written closer to the element. It is not suppressed either: a
|
|
149
|
+
* lint suppression is a counted escape hatch (`pnpm check:hatches`), so
|
|
150
|
+
* silencing a warning that is really a design note would spend four of them, and
|
|
151
|
+
* `biome check .` exits 0 on warnings, so nothing is gated on it.
|
|
152
|
+
*
|
|
153
|
+
* `src/styles-motion.test.ts` asserts this block against the stylesheet SOURCE,
|
|
154
|
+
* a spec having no way to render CSS.
|
|
155
|
+
*/
|
|
156
|
+
@media (prefers-reduced-motion: reduce) {
|
|
157
|
+
*,
|
|
158
|
+
*::before,
|
|
159
|
+
*::after {
|
|
160
|
+
animation-duration: 0.01ms !important;
|
|
161
|
+
animation-iteration-count: 1 !important;
|
|
162
|
+
transition-duration: 0.01ms !important;
|
|
163
|
+
scroll-behavior: auto !important;
|
|
164
|
+
}
|
|
165
|
+
}
|