@heyramzi/ui 3.5.0 → 3.7.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.
Files changed (45) hide show
  1. package/COMPONENTS.md +1 -0
  2. package/dist/ai/chat-window/icons.js +43 -0
  3. package/dist/ai/chat-window/index.js +5 -0
  4. package/dist/ai/chat-window/labels.js +16 -0
  5. package/dist/ai/chat-window/reveal.svelte.js +106 -0
  6. package/dist/ai/chat-window/types.js +4 -0
  7. package/package.json +1 -1
  8. package/src/ai/chat-window/ChatGlyph.svelte +38 -0
  9. package/src/ai/chat-window/ChatGlyph.svelte.d.ts +9 -0
  10. package/src/ai/chat-window/ChatGptWindow.svelte +636 -0
  11. package/src/ai/chat-window/ChatGptWindow.svelte.d.ts +25 -0
  12. package/src/ai/chat-window/ClaudeWindow.svelte +668 -0
  13. package/src/ai/chat-window/ClaudeWindow.svelte.d.ts +25 -0
  14. package/src/ai/chat-window/chat-window.test.ts +80 -0
  15. package/src/ai/chat-window/icons/LICENSE +24 -0
  16. package/src/ai/chat-window/icons/chatgpt-chevron.svg +2 -0
  17. package/src/ai/chat-window/icons/chatgpt-copy.svg +2 -0
  18. package/src/ai/chat-window/icons/chatgpt-mic.svg +2 -0
  19. package/src/ai/chat-window/icons/chatgpt-more.svg +2 -0
  20. package/src/ai/chat-window/icons/chatgpt-plus.svg +2 -0
  21. package/src/ai/chat-window/icons/chatgpt-regenerate.svg +2 -0
  22. package/src/ai/chat-window/icons/chatgpt-share.svg +2 -0
  23. package/src/ai/chat-window/icons/chatgpt-sidebar.svg +1 -0
  24. package/src/ai/chat-window/icons/chatgpt-thumb.svg +2 -0
  25. package/src/ai/chat-window/icons/chatgpt-voice.svg +2 -0
  26. package/src/ai/chat-window/icons/claude-chevron.svg +1 -0
  27. package/src/ai/chat-window/icons/claude-copy.svg +1 -0
  28. package/src/ai/chat-window/icons/claude-plus.svg +1 -0
  29. package/src/ai/chat-window/icons/claude-retry.svg +1 -0
  30. package/src/ai/chat-window/icons/claude-send.svg +1 -0
  31. package/src/ai/chat-window/icons/claude-sidebar.svg +1 -0
  32. package/src/ai/chat-window/icons/claude-sliders.svg +1 -0
  33. package/src/ai/chat-window/icons/claude-spark.svg +1 -0
  34. package/src/ai/chat-window/icons/claude-thumb.svg +1 -0
  35. package/src/ai/chat-window/icons.ts +46 -0
  36. package/src/ai/chat-window/index.ts +26 -0
  37. package/src/ai/chat-window/labels.ts +39 -0
  38. package/src/ai/chat-window/reveal.svelte.ts +106 -0
  39. package/src/ai/chat-window/types.ts +42 -0
  40. package/src/components/open-in/open-in.svelte +14 -15
  41. package/src/components/open-in/open-in.svelte.d.ts +2 -2
  42. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +39 -15
  43. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte.d.ts +2 -1
  44. package/src/shadcn/sidebar/sidebar-provider.svelte +9 -2
  45. package/src/shadcn/sidebar/sidebar-provider.svelte.d.ts +5 -0
@@ -0,0 +1,80 @@
1
+ import { flushSync, mount, unmount } from "svelte";
2
+ import { describe, expect, it } from "vite-plus/test";
3
+ import ChatGptWindow from "./ChatGptWindow.svelte";
4
+ import ClaudeWindow from "./ClaudeWindow.svelte";
5
+ import { REVEAL_BEATS, revealSchedule } from "./reveal.svelte.js";
6
+ import type { ChatMessage } from "./types.js";
7
+
8
+ const messages: ChatMessage[] = [
9
+ { role: "user", text: "How many hours did the team log this week?" },
10
+ { role: "tool", name: "Timesheet", label: "Read week 45" },
11
+ { role: "assistant", text: "The team logged 132 hours." },
12
+ ];
13
+
14
+ function render(Component: typeof ChatGptWindow | typeof ClaudeWindow, props: object) {
15
+ const target = document.createElement("div");
16
+ const app = mount(Component, { target, props: { title: "Hours", messages, ...props } });
17
+ flushSync();
18
+ const root = target.firstElementChild as HTMLElement;
19
+ return { root, done: () => void unmount(app) };
20
+ }
21
+
22
+ describe("revealSchedule", () => {
23
+ it("gives each role its beats in order and ends after the last", () => {
24
+ const { beats, end } = revealSchedule(["user", "tool", "assistant"]);
25
+ expect(beats.map((b) => `${b.index}:${b.phase}`)).toEqual([
26
+ "0:done",
27
+ "1:running",
28
+ "1:done",
29
+ "2:thinking",
30
+ "2:streaming",
31
+ "2:done",
32
+ ]);
33
+ expect(beats[0].at).toBe(REVEAL_BEATS.lead);
34
+ expect(end).toBeGreaterThan(beats.at(-1)?.at ?? 0);
35
+ expect(beats.every((b, i) => i === 0 || b.at > beats[i - 1].at)).toBe(true);
36
+ });
37
+
38
+ it("is empty for an empty conversation", () => {
39
+ expect(revealSchedule([])).toEqual({ beats: [], end: REVEAL_BEATS.lead });
40
+ });
41
+ });
42
+
43
+ describe.each([
44
+ ["ChatGptWindow", ChatGptWindow, "ChatGPT: Hours", "ChatGPT said:"],
45
+ ["ClaudeWindow", ClaudeWindow, "Claude: Hours", "Claude said:"],
46
+ ] as const)("%s", (_, Component, label, said) => {
47
+ it("is a labelled figure a reader gets as text, with no focusable part", () => {
48
+ const { root, done } = render(Component, {});
49
+ expect(root.tagName).toBe("FIGURE");
50
+ expect(root.getAttribute("aria-label")).toBe(label);
51
+ const thread = root.querySelector("ol")?.textContent ?? "";
52
+ expect(thread).toContain("You said:");
53
+ expect(thread).toContain(said);
54
+ expect(thread).toContain("The team logged 132 hours.");
55
+ expect(root.querySelectorAll("button, a, input, textarea, [tabindex]")).toHaveLength(0);
56
+ done();
57
+ });
58
+
59
+ it("names the connector call with its label, finished when nothing plays", () => {
60
+ const { root, done } = render(Component, {});
61
+ const call = root.querySelector(".call")?.textContent ?? "";
62
+ expect(call).toContain("Used Timesheet");
63
+ expect(call).toContain("Read week 45");
64
+ expect(root.querySelectorAll('[data-phase="done"]')).toHaveLength(3);
65
+ done();
66
+ });
67
+
68
+ it("prints a site's own strings and theme", () => {
69
+ const { root, done } = render(Component, {
70
+ theme: "dark",
71
+ placeholder: "Posez votre question",
72
+ labels: { done: "{name} utilisé", userSaid: "Vous avez dit :" },
73
+ });
74
+ expect(root.dataset.mode).toBe("dark");
75
+ expect(root.textContent).toContain("Posez votre question");
76
+ expect(root.querySelector(".call")?.textContent).toContain("Timesheet utilisé");
77
+ expect(root.querySelector("ol")?.textContent).toContain("Vous avez dit :");
78
+ done();
79
+ });
80
+ });
@@ -0,0 +1,24 @@
1
+ Where these icons come from
2
+
3
+ chatgpt-*.svg, except chatgpt-sidebar.svg: Lucide 0.460.0 (lucide-static), ISC licence below.
4
+ Each file names the Lucide icon it is.
5
+ chatgpt-sidebar.svg and claude-*.svg, except claude-spark.svg: drawn for this package.
6
+ claude-spark.svg: Simple Icons "claude", CC0 1.0 (no rights reserved).
7
+
8
+ Lucide licence, as shipped in lucide-static 0.460.0:
9
+
10
+ ISC License
11
+
12
+ Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
13
+
14
+ Permission to use, copy, modify, and/or distribute this software for any
15
+ purpose with or without fee is hereby granted, provided that the above
16
+ copyright notice and this permission notice appear in all copies.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
19
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
20
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
21
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
22
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
23
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
24
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1,2 @@
1
+ <!-- @license lucide-static v0.460.0 - ISC (chevron-down) -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
@@ -0,0 +1,2 @@
1
+ <!-- @license lucide-static v0.460.0 - ISC (copy) -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
@@ -0,0 +1,2 @@
1
+ <!-- @license lucide-static v0.460.0 - ISC (mic) -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>
@@ -0,0 +1,2 @@
1
+ <!-- @license lucide-static v0.460.0 - ISC (ellipsis) -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>
@@ -0,0 +1,2 @@
1
+ <!-- @license lucide-static v0.460.0 - ISC (plus) -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
@@ -0,0 +1,2 @@
1
+ <!-- @license lucide-static v0.460.0 - ISC (refresh-cw) -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg>
@@ -0,0 +1,2 @@
1
+ <!-- @license lucide-static v0.460.0 - ISC (share) -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" x2="12" y1="2" y2="15"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 8h16"/><path d="M4 16h10"/></svg>
@@ -0,0 +1,2 @@
1
+ <!-- @license lucide-static v0.460.0 - ISC (thumbs-up) -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/></svg>
@@ -0,0 +1,2 @@
1
+ <!-- @license lucide-static v0.460.0 - ISC (audio-lines) -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 10v3"/><path d="M6 6v11"/><path d="M10 3v18"/><path d="M14 8v7"/><path d="M18 5v13"/><path d="M22 10v3"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m6 8 4 4 4-4"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7V5.5A1.5 1.5 0 0 1 8.5 4h6A1.5 1.5 0 0 1 16 5.5v6a1.5 1.5 0 0 1-1.5 1.5H13M5.5 7h6A1.5 1.5 0 0 1 13 8.5v6a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 4 14.5v-6A1.5 1.5 0 0 1 5.5 7"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4.5v11M4.5 10h11"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M16 10a6 6 0 1 1-1.76-4.24M16 3.5V6.5h-3"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 15.5v-11M5.5 9 10 4.5 14.5 9"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 5.5A2 2 0 0 1 5.5 3.5h9a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-9a2 2 0 0 1-2-2zM8 3.5v13"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6.5h6m4 0h2M4 13.5h2m4 0h6M12 4.5v4M8 11.5v4"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z" fill="black"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="none" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6.5 9v7.5h-2a1 1 0 0 1-1-1V10a1 1 0 0 1 1-1zm0 0 3-6a2 2 0 0 1 2 2v2.5h3.6a1.5 1.5 0 0 1 1.47 1.8l-1.2 6A1.5 1.5 0 0 1 13.9 16.5H6.5"/></svg>
@@ -0,0 +1,46 @@
1
+ // Each glyph is a file a CSS mask paints in the current colour. Sources and licences: icons/LICENSE.
2
+
3
+ import gptChevron from "./icons/chatgpt-chevron.svg";
4
+ import gptCopy from "./icons/chatgpt-copy.svg";
5
+ import gptMic from "./icons/chatgpt-mic.svg";
6
+ import gptMore from "./icons/chatgpt-more.svg";
7
+ import gptPlus from "./icons/chatgpt-plus.svg";
8
+ import gptRegenerate from "./icons/chatgpt-regenerate.svg";
9
+ import gptShare from "./icons/chatgpt-share.svg";
10
+ import gptSidebar from "./icons/chatgpt-sidebar.svg";
11
+ import gptThumb from "./icons/chatgpt-thumb.svg";
12
+ import gptVoice from "./icons/chatgpt-voice.svg";
13
+ import claudeChevron from "./icons/claude-chevron.svg";
14
+ import claudeCopy from "./icons/claude-copy.svg";
15
+ import claudePlus from "./icons/claude-plus.svg";
16
+ import claudeRetry from "./icons/claude-retry.svg";
17
+ import claudeSend from "./icons/claude-send.svg";
18
+ import claudeSidebar from "./icons/claude-sidebar.svg";
19
+ import claudeSliders from "./icons/claude-sliders.svg";
20
+ import claudeSpark from "./icons/claude-spark.svg";
21
+ import claudeThumb from "./icons/claude-thumb.svg";
22
+
23
+ export const CHATGPT_ICONS = {
24
+ chevron: gptChevron,
25
+ copy: gptCopy,
26
+ mic: gptMic,
27
+ more: gptMore,
28
+ plus: gptPlus,
29
+ regenerate: gptRegenerate,
30
+ share: gptShare,
31
+ sidebar: gptSidebar,
32
+ thumb: gptThumb,
33
+ voice: gptVoice,
34
+ } as const;
35
+
36
+ export const CLAUDE_ICONS = {
37
+ chevron: claudeChevron,
38
+ copy: claudeCopy,
39
+ plus: claudePlus,
40
+ retry: claudeRetry,
41
+ send: claudeSend,
42
+ sidebar: claudeSidebar,
43
+ sliders: claudeSliders,
44
+ spark: claudeSpark,
45
+ thumb: claudeThumb,
46
+ } as const;
@@ -0,0 +1,26 @@
1
+ import ChatGptWindow from "./ChatGptWindow.svelte";
2
+ import ClaudeWindow from "./ClaudeWindow.svelte";
3
+
4
+ export type { ChatGptWindowProps } from "./ChatGptWindow.svelte";
5
+ export type { ClaudeWindowProps } from "./ClaudeWindow.svelte";
6
+ export {
7
+ CHATGPT_LABELS,
8
+ CLAUDE_LABELS,
9
+ type ChatGptLabels,
10
+ type ClaudeLabels,
11
+ } from "./labels.js";
12
+ export {
13
+ REVEAL_BEATS,
14
+ revealSchedule,
15
+ type RevealBeat,
16
+ } from "./reveal.svelte.js";
17
+ export type {
18
+ ChatAssistantMessage,
19
+ ChatMessage,
20
+ ChatPhase,
21
+ ChatRole,
22
+ ChatTheme,
23
+ ChatToolMessage,
24
+ ChatUserMessage,
25
+ } from "./types.js";
26
+ export { ChatGptWindow, ClaudeWindow };
@@ -0,0 +1,39 @@
1
+ // The strings each window prints itself, in a .ts module: a tsc consumer reads .svelte as default-only.
2
+
3
+ /** Every string the ChatGPT window prints itself. `{name}` is the connector's name. */
4
+ export interface ChatGptLabels {
5
+ running: string;
6
+ done: string;
7
+ share: string;
8
+ disclaimer: string;
9
+ userSaid: string;
10
+ assistantSaid: string;
11
+ }
12
+
13
+ export const CHATGPT_LABELS: ChatGptLabels = {
14
+ running: "Calling {name}",
15
+ done: "Used {name}",
16
+ share: "Share",
17
+ disclaimer: "ChatGPT can make mistakes. Check important info.",
18
+ userSaid: "You said:",
19
+ assistantSaid: "ChatGPT said:",
20
+ };
21
+
22
+ /** Every string the Claude window prints itself. `{name}` is the connector's name. */
23
+ export interface ClaudeLabels {
24
+ running: string;
25
+ done: string;
26
+ share: string;
27
+ disclaimer: string;
28
+ userSaid: string;
29
+ assistantSaid: string;
30
+ }
31
+
32
+ export const CLAUDE_LABELS: ClaudeLabels = {
33
+ running: "Using {name}",
34
+ done: "Used {name}",
35
+ share: "Share",
36
+ disclaimer: "Claude can make mistakes. Please double-check responses.",
37
+ userSaid: "You said:",
38
+ assistantSaid: "Claude said:",
39
+ };
@@ -0,0 +1,106 @@
1
+ import { untrack } from "svelte";
2
+ import type { ChatPhase, ChatRole } from "./types.js";
3
+
4
+ /** Milliseconds each beat holds, read as one conversation's pacing, not a transition. */
5
+ export const REVEAL_BEATS = {
6
+ lead: 400,
7
+ user: 900,
8
+ thinking: 900,
9
+ stream: 1500,
10
+ running: 1300,
11
+ after: 350,
12
+ } as const;
13
+
14
+ export interface RevealBeat {
15
+ at: number;
16
+ index: number;
17
+ phase: ChatPhase;
18
+ }
19
+
20
+ /** The timeline for a conversation: when each message enters and settles, and when it ends. */
21
+ export function revealSchedule(
22
+ roles: readonly ChatRole[],
23
+ beats: typeof REVEAL_BEATS = REVEAL_BEATS,
24
+ ): { beats: RevealBeat[]; end: number } {
25
+ const out: RevealBeat[] = [];
26
+ let t = beats.lead;
27
+ roles.forEach((role, index) => {
28
+ if (role === "user") {
29
+ out.push({ at: t, index, phase: "done" });
30
+ t += beats.user;
31
+ } else if (role === "assistant") {
32
+ out.push({ at: t, index, phase: "thinking" });
33
+ t += beats.thinking;
34
+ out.push({ at: t, index, phase: "streaming" });
35
+ t += beats.stream;
36
+ out.push({ at: t, index, phase: "done" });
37
+ t += beats.after;
38
+ } else {
39
+ out.push({ at: t, index, phase: "running" });
40
+ t += beats.running;
41
+ out.push({ at: t, index, phase: "done" });
42
+ t += beats.after;
43
+ }
44
+ });
45
+ return { beats: out, end: t };
46
+ }
47
+
48
+ interface RevealOptions {
49
+ roles: readonly ChatRole[];
50
+ enabled: boolean;
51
+ active: boolean;
52
+ hold: number;
53
+ ondone?: () => void;
54
+ }
55
+
56
+ /**
57
+ * Plays a conversation once the window is on screen and `active`, again each time `active` turns on.
58
+ * Renders finished for SSR, no JavaScript and reduced motion.
59
+ */
60
+ export function createReveal(options: () => RevealOptions) {
61
+ let phases = $state<ChatPhase[]>([]);
62
+ let node = $state<HTMLElement | null>(null);
63
+ let seen = $state(false);
64
+
65
+ $effect(() => {
66
+ const { enabled } = options();
67
+ if (!node || !enabled || seen) return;
68
+ const io = new IntersectionObserver(
69
+ (entries) => {
70
+ if (entries.some((e) => e.isIntersecting)) {
71
+ seen = true;
72
+ io.disconnect();
73
+ }
74
+ },
75
+ { threshold: 0.35 },
76
+ );
77
+ io.observe(node);
78
+ return () => io.disconnect();
79
+ });
80
+
81
+ $effect(() => {
82
+ const { enabled, active } = options();
83
+ if (!enabled || !active || !seen) return;
84
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
85
+ const { roles, hold, ondone } = untrack(options);
86
+ const { beats, end } = revealSchedule(roles);
87
+ phases = roles.map(() => "hidden");
88
+ const timers = beats.map((b) => setTimeout(() => (phases[b.index] = b.phase), b.at));
89
+ timers.push(setTimeout(() => ondone?.(), end + hold));
90
+ return () => {
91
+ timers.forEach(clearTimeout);
92
+ phases = [];
93
+ };
94
+ });
95
+
96
+ return {
97
+ /** A message's phase; `done` when nothing is playing. */
98
+ phase: (index: number): ChatPhase => phases[index] ?? "done",
99
+ /** Attach to the window's root, so the reveal waits until it scrolls into view. */
100
+ attach: (el: HTMLElement) => {
101
+ node = el;
102
+ return () => (node = null);
103
+ },
104
+ streamMs: REVEAL_BEATS.stream,
105
+ };
106
+ }
@@ -0,0 +1,42 @@
1
+ import type { Snippet } from "svelte";
2
+
3
+ /** What the person typed. */
4
+ export interface ChatUserMessage {
5
+ role: "user";
6
+ text: string;
7
+ }
8
+
9
+ /** The model's answer: plain text, or a snippet for a table, a figure or a list. */
10
+ export interface ChatAssistantMessage {
11
+ role: "assistant";
12
+ text?: string;
13
+ content?: Snippet;
14
+ }
15
+
16
+ /** One call to a connector, drawn as the product draws it. */
17
+ export interface ChatToolMessage {
18
+ role: "tool";
19
+ /** The connector or app, as the product names it ("Locameublé"). */
20
+ name: string;
21
+ /** What the call did, short ("Read the 2026 result"). */
22
+ label?: string;
23
+ /** The connector's mark, about 16 to 20px. */
24
+ icon?: Snippet;
25
+ /** Or a URL for it, drawn as a small rounded image. */
26
+ iconSrc?: string;
27
+ }
28
+
29
+ export type ChatMessage = ChatUserMessage | ChatAssistantMessage | ChatToolMessage;
30
+
31
+ export type ChatRole = ChatMessage["role"];
32
+
33
+ /** `auto` follows the page: a `.dark` or `data-theme` ancestor, else the system setting. */
34
+ export type ChatTheme = "light" | "dark" | "auto";
35
+
36
+ /** Where one message stands while the reveal plays. */
37
+ export type ChatPhase = "hidden" | "thinking" | "streaming" | "running" | "done";
38
+
39
+ /** Fills `{name}` in a label such as "Used {name}". */
40
+ export function fillName(template: string, name: string): string {
41
+ return template.replaceAll("{name}", name);
42
+ }
@@ -14,8 +14,8 @@
14
14
  }
15
15
 
16
16
  /** `chip` is the bordered inline tag, `button` sits in a header beside other
17
- * buttons, `icon` is the bare mark for a dense row. */
18
- export type OpenInVariant = "chip" | "button" | "icon";
17
+ * buttons. A bare-mark form was cut: at 20px it failed WCAG 2.5.8. */
18
+ export type OpenInVariant = "chip" | "button";
19
19
  export type OpenInSize = "sm" | "default";
20
20
  </script>
21
21
 
@@ -86,22 +86,21 @@
86
86
  class={cn(
87
87
  variant === "button"
88
88
  ? buttonVariants({ variant: "outline", size: size === "sm" ? "sm" : "default" })
89
- : variant === "icon"
90
- ? "inline-flex shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
91
- : cn(
92
- "inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap rounded-md border border-border text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
93
- size === "sm" ? "px-1.5 py-0.5 text-2xs" : "px-2 py-1 text-xs",
94
- ),
95
- variant === "icon" && (size === "sm" ? "size-5" : "size-6"),
89
+ : cn(
90
+ "inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap rounded-md border border-border text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
91
+ // WHY the button heights: the sm chip was 19px, under the 24px target
92
+ // floor. Under a thumb it grows to 40 and 44, and the sm hit area to 44.
93
+ size === "sm"
94
+ ? "relative h-7 px-2 text-2xs after:absolute after:top-1/2 after:left-1/2 after:h-[max(100%,1.5rem)] after:w-[max(100%,1.5rem)] after:-translate-x-1/2 after:-translate-y-1/2 any-pointer-coarse:h-10 any-pointer-coarse:after:h-[max(100%,2.75rem)] any-pointer-coarse:after:w-[max(100%,2.75rem)]"
95
+ : "h-8 px-2.5 text-xs any-pointer-coarse:h-11 any-pointer-coarse:px-3",
96
+ ),
96
97
  className,
97
98
  )}
98
99
  >
99
100
  <Icon aria-hidden="true" size={iconSize} />
100
- {#if variant !== "icon"}
101
- {text}
102
- <!-- The one thing the mark can't say: this leaves the app. It stays muted
103
- so the eye lands on the tool name first. -->
104
- <ArrowSquareOut aria-hidden="true" size={iconSize} class="text-muted-foreground" />
105
- {/if}
101
+ {text}
102
+ <!-- The one thing the mark can't say: this leaves the app. It stays muted
103
+ so the eye lands on the tool name first. -->
104
+ <ArrowSquareOut aria-hidden="true" size={iconSize} class="text-muted-foreground" />
106
105
  </a>
107
106
  </div>
@@ -15,8 +15,8 @@ export interface OpenInTool {
15
15
  }>;
16
16
  }
17
17
  /** `chip` is the bordered inline tag, `button` sits in a header beside other
18
- * buttons, `icon` is the bare mark for a dense row. */
19
- export type OpenInVariant = "chip" | "button" | "icon";
18
+ * buttons. A bare-mark form was cut: at 20px it failed WCAG 2.5.8. */
19
+ export type OpenInVariant = "chip" | "button";
20
20
  export type OpenInSize = "sm" | "default";
21
21
  interface Props {
22
22
  /** The tool to link to: its name, accessible sentence and mark. */
@@ -12,7 +12,8 @@
12
12
  /** `submit` posts the form the button sits in. */
13
13
  type?: "button" | "submit";
14
14
  disabled?: boolean;
15
- size?: "default" | "md" | "sm";
15
+ /** `sm` is 40px for a condensed bar; under a thumb both are 44px. */
16
+ size?: "default" | "sm";
16
17
  tone?: "ink" | "coral";
17
18
  beam?: boolean;
18
19
  dot?: boolean;
@@ -43,7 +44,6 @@
43
44
  let classes = $derived(
44
45
  [
45
46
  "cta-hover",
46
- size === "md" && "cta-hover--md",
47
47
  size === "sm" && "cta-hover--sm",
48
48
  tone === "coral" && "cta-hover--coral",
49
49
  !beam && "cta-hover--no-beam",
@@ -168,28 +168,24 @@
168
168
  pointer-events: none;
169
169
  }
170
170
 
171
- /* 40px, between 44px and 32px, for a CTA in a condensed bar. 32px is too
172
- small to read as the main action, so only the label shrinks, to 13px. */
173
- .cta-hover--md {
171
+ /* 40px for a CTA in a condensed bar. The old 32px was too small to read as
172
+ the main action, so only the label shrinks, to 13px. */
173
+ .cta-hover--sm {
174
174
  height: 40px;
175
175
  padding: 0 24px;
176
176
  font-size: 13px;
177
177
  }
178
178
 
179
- .cta-hover--md .cta-hover__dot {
179
+ .cta-hover--sm .cta-hover__dot {
180
180
  width: 7px;
181
181
  height: 7px;
182
182
  }
183
183
 
184
- .cta-hover--sm {
185
- height: 32px;
186
- padding: 0 20px;
187
- font-size: 13px;
188
- }
189
-
190
- .cta-hover--sm .cta-hover__dot {
191
- width: 6px;
192
- height: 6px;
184
+ /* Under a thumb both sizes clear the 44px touch target, as Button does. */
185
+ @media (any-pointer: coarse) {
186
+ .cta-hover--sm {
187
+ height: 44px;
188
+ }
193
189
  }
194
190
 
195
191
  .cta-hover--no-beam::before {
@@ -344,11 +340,39 @@
344
340
  }
345
341
  }
346
342
 
343
+ /* The beam stops, and hover swaps the fill and label in place: no dot
344
+ growing out, no label sliding across. */
347
345
  @media (prefers-reduced-motion: reduce) {
348
346
  .cta-hover::before {
349
347
  animation: none;
350
348
  opacity: 0;
351
349
  }
350
+
351
+ .cta-hover,
352
+ .cta-hover__resting,
353
+ .cta-hover__dot,
354
+ .cta-hover__label,
355
+ .cta-hover__reveal,
356
+ :global(.cta-hover__arrow--inline) {
357
+ transition-property: opacity, background-color, color;
358
+ }
359
+ }
360
+
361
+ @media (prefers-reduced-motion: reduce) and (hover: hover) and (pointer: fine) {
362
+ .cta-hover:hover .cta-hover__dot,
363
+ .cta-hover:not(.cta-hover--no-dot):hover .cta-hover__resting .cta-hover__label,
364
+ .cta-hover:not(.cta-hover--no-dot):hover .cta-hover__reveal,
365
+ .cta-hover--no-dot:hover :global(.cta-hover__arrow--inline) {
366
+ transform: none;
367
+ }
368
+
369
+ .cta-hover:not(.cta-hover--no-dot):hover::after {
370
+ background: var(--color-signal-600);
371
+ }
372
+
373
+ .cta-hover--coral:not(.cta-hover--no-dot):hover::after {
374
+ background: var(--color-ink);
375
+ }
352
376
  }
353
377
  }
354
378
 
@@ -3,7 +3,8 @@ interface Props {
3
3
  /** `submit` posts the form the button sits in. */
4
4
  type?: "button" | "submit";
5
5
  disabled?: boolean;
6
- size?: "default" | "md" | "sm";
6
+ /** `sm` is 40px for a condensed bar; under a thumb both are 44px. */
7
+ size?: "default" | "sm";
7
8
  tone?: "ink" | "coral";
8
9
  beam?: boolean;
9
10
  dot?: boolean;