@heyramzi/ui 3.4.0 → 3.6.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 (305) hide show
  1. package/COMPONENTS.md +2 -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/dist/components/copy/clipboard.js +3 -3
  8. package/dist/components/flag/codes.js +13 -0
  9. package/dist/components/flag/index.js +2 -0
  10. package/dist/slides/export/pdf.js +7 -0
  11. package/dist/vite/flags.js +46 -0
  12. package/package.json +7 -2
  13. package/src/ai/chat-window/ChatGlyph.svelte +38 -0
  14. package/src/ai/chat-window/ChatGlyph.svelte.d.ts +9 -0
  15. package/src/ai/chat-window/ChatGptWindow.svelte +636 -0
  16. package/src/ai/chat-window/ChatGptWindow.svelte.d.ts +25 -0
  17. package/src/ai/chat-window/ClaudeWindow.svelte +668 -0
  18. package/src/ai/chat-window/ClaudeWindow.svelte.d.ts +25 -0
  19. package/src/ai/chat-window/chat-window.test.ts +80 -0
  20. package/src/ai/chat-window/icons/LICENSE +24 -0
  21. package/src/ai/chat-window/icons/chatgpt-chevron.svg +2 -0
  22. package/src/ai/chat-window/icons/chatgpt-copy.svg +2 -0
  23. package/src/ai/chat-window/icons/chatgpt-mic.svg +2 -0
  24. package/src/ai/chat-window/icons/chatgpt-more.svg +2 -0
  25. package/src/ai/chat-window/icons/chatgpt-plus.svg +2 -0
  26. package/src/ai/chat-window/icons/chatgpt-regenerate.svg +2 -0
  27. package/src/ai/chat-window/icons/chatgpt-share.svg +2 -0
  28. package/src/ai/chat-window/icons/chatgpt-sidebar.svg +1 -0
  29. package/src/ai/chat-window/icons/chatgpt-thumb.svg +2 -0
  30. package/src/ai/chat-window/icons/chatgpt-voice.svg +2 -0
  31. package/src/ai/chat-window/icons/claude-chevron.svg +1 -0
  32. package/src/ai/chat-window/icons/claude-copy.svg +1 -0
  33. package/src/ai/chat-window/icons/claude-plus.svg +1 -0
  34. package/src/ai/chat-window/icons/claude-retry.svg +1 -0
  35. package/src/ai/chat-window/icons/claude-send.svg +1 -0
  36. package/src/ai/chat-window/icons/claude-sidebar.svg +1 -0
  37. package/src/ai/chat-window/icons/claude-sliders.svg +1 -0
  38. package/src/ai/chat-window/icons/claude-spark.svg +1 -0
  39. package/src/ai/chat-window/icons/claude-thumb.svg +1 -0
  40. package/src/ai/chat-window/icons.ts +46 -0
  41. package/src/ai/chat-window/index.ts +26 -0
  42. package/src/ai/chat-window/labels.ts +39 -0
  43. package/src/ai/chat-window/reveal.svelte.ts +106 -0
  44. package/src/ai/chat-window/types.ts +42 -0
  45. package/src/components/copy/clipboard.test.ts +17 -1
  46. package/src/components/copy/clipboard.ts +3 -3
  47. package/src/components/flag/codes.test.ts +30 -0
  48. package/src/components/flag/codes.ts +25 -0
  49. package/src/components/flag/flag.svelte +51 -0
  50. package/src/components/flag/flag.svelte.d.ts +12 -0
  51. package/src/components/flag/flags/AD.svg +1 -0
  52. package/src/components/flag/flags/AE.svg +1 -0
  53. package/src/components/flag/flags/AF.svg +1 -0
  54. package/src/components/flag/flags/AG.svg +1 -0
  55. package/src/components/flag/flags/AI.svg +1 -0
  56. package/src/components/flag/flags/AL.svg +1 -0
  57. package/src/components/flag/flags/AM.svg +1 -0
  58. package/src/components/flag/flags/AO.svg +1 -0
  59. package/src/components/flag/flags/AQ.svg +1 -0
  60. package/src/components/flag/flags/AR.svg +1 -0
  61. package/src/components/flag/flags/AS.svg +1 -0
  62. package/src/components/flag/flags/AT.svg +1 -0
  63. package/src/components/flag/flags/AU.svg +1 -0
  64. package/src/components/flag/flags/AW.svg +1 -0
  65. package/src/components/flag/flags/AX.svg +1 -0
  66. package/src/components/flag/flags/AZ.svg +1 -0
  67. package/src/components/flag/flags/BA.svg +1 -0
  68. package/src/components/flag/flags/BB.svg +1 -0
  69. package/src/components/flag/flags/BD.svg +1 -0
  70. package/src/components/flag/flags/BE.svg +1 -0
  71. package/src/components/flag/flags/BF.svg +1 -0
  72. package/src/components/flag/flags/BG.svg +1 -0
  73. package/src/components/flag/flags/BH.svg +1 -0
  74. package/src/components/flag/flags/BI.svg +1 -0
  75. package/src/components/flag/flags/BJ.svg +1 -0
  76. package/src/components/flag/flags/BL.svg +1 -0
  77. package/src/components/flag/flags/BM.svg +1 -0
  78. package/src/components/flag/flags/BN.svg +1 -0
  79. package/src/components/flag/flags/BO.svg +1 -0
  80. package/src/components/flag/flags/BQ.svg +1 -0
  81. package/src/components/flag/flags/BR.svg +1 -0
  82. package/src/components/flag/flags/BS.svg +1 -0
  83. package/src/components/flag/flags/BT.svg +1 -0
  84. package/src/components/flag/flags/BV.svg +1 -0
  85. package/src/components/flag/flags/BW.svg +1 -0
  86. package/src/components/flag/flags/BY.svg +1 -0
  87. package/src/components/flag/flags/BZ.svg +1 -0
  88. package/src/components/flag/flags/CA.svg +1 -0
  89. package/src/components/flag/flags/CC.svg +1 -0
  90. package/src/components/flag/flags/CD.svg +1 -0
  91. package/src/components/flag/flags/CF.svg +1 -0
  92. package/src/components/flag/flags/CG.svg +1 -0
  93. package/src/components/flag/flags/CH.svg +1 -0
  94. package/src/components/flag/flags/CI.svg +1 -0
  95. package/src/components/flag/flags/CK.svg +1 -0
  96. package/src/components/flag/flags/CL.svg +1 -0
  97. package/src/components/flag/flags/CM.svg +1 -0
  98. package/src/components/flag/flags/CN.svg +1 -0
  99. package/src/components/flag/flags/CO.svg +1 -0
  100. package/src/components/flag/flags/CR.svg +1 -0
  101. package/src/components/flag/flags/CU.svg +1 -0
  102. package/src/components/flag/flags/CV.svg +1 -0
  103. package/src/components/flag/flags/CW.svg +1 -0
  104. package/src/components/flag/flags/CX.svg +1 -0
  105. package/src/components/flag/flags/CY.svg +1 -0
  106. package/src/components/flag/flags/CZ.svg +1 -0
  107. package/src/components/flag/flags/DE.svg +1 -0
  108. package/src/components/flag/flags/DJ.svg +1 -0
  109. package/src/components/flag/flags/DK.svg +1 -0
  110. package/src/components/flag/flags/DM.svg +1 -0
  111. package/src/components/flag/flags/DO.svg +1 -0
  112. package/src/components/flag/flags/DZ.svg +1 -0
  113. package/src/components/flag/flags/EC.svg +1 -0
  114. package/src/components/flag/flags/EE.svg +1 -0
  115. package/src/components/flag/flags/EG.svg +1 -0
  116. package/src/components/flag/flags/EH.svg +1 -0
  117. package/src/components/flag/flags/ER.svg +1 -0
  118. package/src/components/flag/flags/ES.svg +1 -0
  119. package/src/components/flag/flags/ET.svg +1 -0
  120. package/src/components/flag/flags/FI.svg +1 -0
  121. package/src/components/flag/flags/FJ.svg +1 -0
  122. package/src/components/flag/flags/FK.svg +1 -0
  123. package/src/components/flag/flags/FM.svg +1 -0
  124. package/src/components/flag/flags/FO.svg +1 -0
  125. package/src/components/flag/flags/FR.svg +1 -0
  126. package/src/components/flag/flags/GA.svg +1 -0
  127. package/src/components/flag/flags/GB.svg +1 -0
  128. package/src/components/flag/flags/GD.svg +1 -0
  129. package/src/components/flag/flags/GE.svg +1 -0
  130. package/src/components/flag/flags/GF.svg +1 -0
  131. package/src/components/flag/flags/GG.svg +1 -0
  132. package/src/components/flag/flags/GH.svg +1 -0
  133. package/src/components/flag/flags/GI.svg +1 -0
  134. package/src/components/flag/flags/GL.svg +1 -0
  135. package/src/components/flag/flags/GM.svg +1 -0
  136. package/src/components/flag/flags/GN.svg +1 -0
  137. package/src/components/flag/flags/GP.svg +1 -0
  138. package/src/components/flag/flags/GQ.svg +1 -0
  139. package/src/components/flag/flags/GR.svg +1 -0
  140. package/src/components/flag/flags/GS.svg +1 -0
  141. package/src/components/flag/flags/GT.svg +1 -0
  142. package/src/components/flag/flags/GU.svg +1 -0
  143. package/src/components/flag/flags/GW.svg +1 -0
  144. package/src/components/flag/flags/GY.svg +1 -0
  145. package/src/components/flag/flags/HK.svg +1 -0
  146. package/src/components/flag/flags/HM.svg +1 -0
  147. package/src/components/flag/flags/HN.svg +1 -0
  148. package/src/components/flag/flags/HR.svg +1 -0
  149. package/src/components/flag/flags/HT.svg +1 -0
  150. package/src/components/flag/flags/HU.svg +1 -0
  151. package/src/components/flag/flags/ID.svg +1 -0
  152. package/src/components/flag/flags/IE.svg +1 -0
  153. package/src/components/flag/flags/IL.svg +1 -0
  154. package/src/components/flag/flags/IM.svg +1 -0
  155. package/src/components/flag/flags/IN.svg +1 -0
  156. package/src/components/flag/flags/IO.svg +1 -0
  157. package/src/components/flag/flags/IQ.svg +1 -0
  158. package/src/components/flag/flags/IR.svg +1 -0
  159. package/src/components/flag/flags/IS.svg +1 -0
  160. package/src/components/flag/flags/IT.svg +1 -0
  161. package/src/components/flag/flags/JE.svg +1 -0
  162. package/src/components/flag/flags/JM.svg +1 -0
  163. package/src/components/flag/flags/JO.svg +1 -0
  164. package/src/components/flag/flags/JP.svg +1 -0
  165. package/src/components/flag/flags/KE.svg +1 -0
  166. package/src/components/flag/flags/KG.svg +1 -0
  167. package/src/components/flag/flags/KH.svg +1 -0
  168. package/src/components/flag/flags/KI.svg +1 -0
  169. package/src/components/flag/flags/KM.svg +1 -0
  170. package/src/components/flag/flags/KN.svg +1 -0
  171. package/src/components/flag/flags/KP.svg +1 -0
  172. package/src/components/flag/flags/KR.svg +1 -0
  173. package/src/components/flag/flags/KW.svg +1 -0
  174. package/src/components/flag/flags/KY.svg +1 -0
  175. package/src/components/flag/flags/KZ.svg +1 -0
  176. package/src/components/flag/flags/LA.svg +1 -0
  177. package/src/components/flag/flags/LB.svg +1 -0
  178. package/src/components/flag/flags/LC.svg +1 -0
  179. package/src/components/flag/flags/LI.svg +1 -0
  180. package/src/components/flag/flags/LICENSE +22 -0
  181. package/src/components/flag/flags/LK.svg +1 -0
  182. package/src/components/flag/flags/LR.svg +1 -0
  183. package/src/components/flag/flags/LS.svg +1 -0
  184. package/src/components/flag/flags/LT.svg +1 -0
  185. package/src/components/flag/flags/LU.svg +1 -0
  186. package/src/components/flag/flags/LV.svg +1 -0
  187. package/src/components/flag/flags/LY.svg +1 -0
  188. package/src/components/flag/flags/MA.svg +1 -0
  189. package/src/components/flag/flags/MC.svg +1 -0
  190. package/src/components/flag/flags/MD.svg +1 -0
  191. package/src/components/flag/flags/ME.svg +1 -0
  192. package/src/components/flag/flags/MF.svg +1 -0
  193. package/src/components/flag/flags/MG.svg +1 -0
  194. package/src/components/flag/flags/MH.svg +1 -0
  195. package/src/components/flag/flags/MK.svg +1 -0
  196. package/src/components/flag/flags/ML.svg +1 -0
  197. package/src/components/flag/flags/MM.svg +1 -0
  198. package/src/components/flag/flags/MN.svg +1 -0
  199. package/src/components/flag/flags/MO.svg +1 -0
  200. package/src/components/flag/flags/MP.svg +1 -0
  201. package/src/components/flag/flags/MQ.svg +1 -0
  202. package/src/components/flag/flags/MR.svg +1 -0
  203. package/src/components/flag/flags/MS.svg +1 -0
  204. package/src/components/flag/flags/MT.svg +1 -0
  205. package/src/components/flag/flags/MU.svg +1 -0
  206. package/src/components/flag/flags/MV.svg +1 -0
  207. package/src/components/flag/flags/MW.svg +1 -0
  208. package/src/components/flag/flags/MX.svg +1 -0
  209. package/src/components/flag/flags/MY.svg +1 -0
  210. package/src/components/flag/flags/MZ.svg +1 -0
  211. package/src/components/flag/flags/NA.svg +1 -0
  212. package/src/components/flag/flags/NC.svg +1 -0
  213. package/src/components/flag/flags/NE.svg +1 -0
  214. package/src/components/flag/flags/NF.svg +1 -0
  215. package/src/components/flag/flags/NG.svg +1 -0
  216. package/src/components/flag/flags/NI.svg +1 -0
  217. package/src/components/flag/flags/NL.svg +1 -0
  218. package/src/components/flag/flags/NO.svg +1 -0
  219. package/src/components/flag/flags/NP.svg +1 -0
  220. package/src/components/flag/flags/NR.svg +1 -0
  221. package/src/components/flag/flags/NU.svg +1 -0
  222. package/src/components/flag/flags/NZ.svg +1 -0
  223. package/src/components/flag/flags/OM.svg +1 -0
  224. package/src/components/flag/flags/PA.svg +1 -0
  225. package/src/components/flag/flags/PE.svg +1 -0
  226. package/src/components/flag/flags/PF.svg +1 -0
  227. package/src/components/flag/flags/PG.svg +1 -0
  228. package/src/components/flag/flags/PH.svg +1 -0
  229. package/src/components/flag/flags/PK.svg +1 -0
  230. package/src/components/flag/flags/PL.svg +1 -0
  231. package/src/components/flag/flags/PM.svg +1 -0
  232. package/src/components/flag/flags/PN.svg +1 -0
  233. package/src/components/flag/flags/PR.svg +1 -0
  234. package/src/components/flag/flags/PS.svg +1 -0
  235. package/src/components/flag/flags/PT.svg +1 -0
  236. package/src/components/flag/flags/PW.svg +1 -0
  237. package/src/components/flag/flags/PY.svg +1 -0
  238. package/src/components/flag/flags/QA.svg +1 -0
  239. package/src/components/flag/flags/RE.svg +1 -0
  240. package/src/components/flag/flags/RO.svg +1 -0
  241. package/src/components/flag/flags/RS.svg +1 -0
  242. package/src/components/flag/flags/RU.svg +1 -0
  243. package/src/components/flag/flags/RW.svg +1 -0
  244. package/src/components/flag/flags/SA.svg +1 -0
  245. package/src/components/flag/flags/SB.svg +1 -0
  246. package/src/components/flag/flags/SC.svg +1 -0
  247. package/src/components/flag/flags/SD.svg +1 -0
  248. package/src/components/flag/flags/SE.svg +1 -0
  249. package/src/components/flag/flags/SG.svg +1 -0
  250. package/src/components/flag/flags/SH.svg +1 -0
  251. package/src/components/flag/flags/SI.svg +1 -0
  252. package/src/components/flag/flags/SJ.svg +1 -0
  253. package/src/components/flag/flags/SK.svg +1 -0
  254. package/src/components/flag/flags/SL.svg +1 -0
  255. package/src/components/flag/flags/SM.svg +1 -0
  256. package/src/components/flag/flags/SN.svg +1 -0
  257. package/src/components/flag/flags/SO.svg +1 -0
  258. package/src/components/flag/flags/SR.svg +1 -0
  259. package/src/components/flag/flags/SS.svg +1 -0
  260. package/src/components/flag/flags/ST.svg +1 -0
  261. package/src/components/flag/flags/SV.svg +1 -0
  262. package/src/components/flag/flags/SX.svg +1 -0
  263. package/src/components/flag/flags/SY.svg +1 -0
  264. package/src/components/flag/flags/SZ.svg +1 -0
  265. package/src/components/flag/flags/TC.svg +1 -0
  266. package/src/components/flag/flags/TD.svg +1 -0
  267. package/src/components/flag/flags/TF.svg +1 -0
  268. package/src/components/flag/flags/TG.svg +1 -0
  269. package/src/components/flag/flags/TH.svg +1 -0
  270. package/src/components/flag/flags/TJ.svg +1 -0
  271. package/src/components/flag/flags/TK.svg +1 -0
  272. package/src/components/flag/flags/TL.svg +1 -0
  273. package/src/components/flag/flags/TM.svg +1 -0
  274. package/src/components/flag/flags/TN.svg +1 -0
  275. package/src/components/flag/flags/TO.svg +1 -0
  276. package/src/components/flag/flags/TR.svg +1 -0
  277. package/src/components/flag/flags/TT.svg +1 -0
  278. package/src/components/flag/flags/TV.svg +1 -0
  279. package/src/components/flag/flags/TW.svg +1 -0
  280. package/src/components/flag/flags/TZ.svg +1 -0
  281. package/src/components/flag/flags/UA.svg +1 -0
  282. package/src/components/flag/flags/UG.svg +1 -0
  283. package/src/components/flag/flags/UM.svg +1 -0
  284. package/src/components/flag/flags/US.svg +1 -0
  285. package/src/components/flag/flags/UY.svg +1 -0
  286. package/src/components/flag/flags/UZ.svg +1 -0
  287. package/src/components/flag/flags/VA.svg +1 -0
  288. package/src/components/flag/flags/VC.svg +1 -0
  289. package/src/components/flag/flags/VE.svg +1 -0
  290. package/src/components/flag/flags/VG.svg +1 -0
  291. package/src/components/flag/flags/VI.svg +1 -0
  292. package/src/components/flag/flags/VN.svg +1 -0
  293. package/src/components/flag/flags/VU.svg +1 -0
  294. package/src/components/flag/flags/WF.svg +1 -0
  295. package/src/components/flag/flags/WS.svg +1 -0
  296. package/src/components/flag/flags/XK.svg +1 -0
  297. package/src/components/flag/flags/YE.svg +1 -0
  298. package/src/components/flag/flags/YT.svg +1 -0
  299. package/src/components/flag/flags/ZA.svg +1 -0
  300. package/src/components/flag/flags/ZM.svg +1 -0
  301. package/src/components/flag/flags/ZW.svg +1 -0
  302. package/src/components/flag/index.ts +2 -0
  303. package/src/slides/export/pdf.ts +11 -0
  304. package/src/vite/flags.test.ts +38 -0
  305. package/src/vite/flags.ts +63 -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
+ }
@@ -1,5 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
2
- import { copyRichText, copyText } from "./clipboard.js";
2
+ import { copyRichText, copyText, htmlToPlainText } from "./clipboard.js";
3
3
 
4
4
  // Mock navigator.clipboard
5
5
  const mockClipboard = {
@@ -204,3 +204,19 @@ describe("htmlToPlainText (server-side)", () => {
204
204
  expect(mockWriteText).toHaveBeenCalledWith("Hello World & Friends <3>");
205
205
  });
206
206
  });
207
+
208
+ describe("htmlToPlainText (browser)", () => {
209
+ afterEach(() => vi.restoreAllMocks());
210
+
211
+ it("parses into an inert document, so an onerror in the markup never runs", async () => {
212
+ const win = window as Window & { __fired?: boolean };
213
+ win.__fired = false;
214
+ const createElement = vi.spyOn(document, "createElement");
215
+ expect(htmlToPlainText('<img src="x" onerror="window.__fired=true">hi')).toBe("hi");
216
+ await new Promise((done) => setTimeout(done, 10));
217
+ expect(win.__fired).toBe(false);
218
+ // jsdom loads no images, so the onerror can't fire here either way: the live-document
219
+ // element the old code built is what a real browser runs the handler on.
220
+ expect(createElement).not.toHaveBeenCalled();
221
+ });
222
+ });
@@ -47,7 +47,7 @@ export function htmlToPlainText(html: string): string {
47
47
  .replace(/&gt;/g, ">")
48
48
  .trim();
49
49
  }
50
- const div = document.createElement("div");
51
- div.innerHTML = html;
52
- return div.textContent || div.innerText || "";
50
+ // A DOMParser document has no browsing context, so nothing in the markup loads or runs.
51
+ // An element from document.createElement belongs to the live page: an <img onerror> fires.
52
+ return new DOMParser().parseFromString(html, "text/html").body.textContent ?? "";
53
53
  }
@@ -0,0 +1,30 @@
1
+ import { readdirSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { describe, expect, it } from "vite-plus/test";
5
+ import { FLAG_CODES, flagCode } from "./codes.js";
6
+
7
+ const dir = join(dirname(fileURLToPath(import.meta.url)), "flags");
8
+
9
+ describe("flag codes", () => {
10
+ it("lists exactly the SVG files shipped", () => {
11
+ const files = readdirSync(dir)
12
+ .filter((name) => name.endsWith(".svg"))
13
+ .map((name) => name.slice(0, -4));
14
+ expect(FLAG_CODES.toSorted()).toEqual(files.toSorted());
15
+ expect(FLAG_CODES).toHaveLength(250);
16
+ });
17
+
18
+ it("knows Kosovo, which ISO 3166-1 leaves user-assigned", () => {
19
+ expect(flagCode("xk")).toBe("XK");
20
+ });
21
+
22
+ it("takes any case and refuses what it has no file for", () => {
23
+ expect(flagCode("jp")).toBe("JP");
24
+ expect(flagCode(" Pl ")).toBe("PL");
25
+ expect(flagCode("")).toBeNull();
26
+ expect(flagCode(undefined)).toBeNull();
27
+ expect(flagCode("ZZ")).toBeNull();
28
+ expect(flagCode("../FR")).toBeNull();
29
+ });
30
+ });
@@ -0,0 +1,25 @@
1
+ /** Where the host serves the flag files. `flagAssets()` from `@heyramzi/ui/vite` serves them there. */
2
+ export const FLAG_BASE = "/flags";
3
+
4
+ /**
5
+ * The 249 ISO 3166-1 alpha-2 codes plus XK (Kosovo, user-assigned but in common use), each with
6
+ * a file in `./flags`, from country-flag-icons 1.6.20 (MIT).
7
+ */
8
+ export const FLAG_CODES: readonly string[] = (
9
+ "AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR " +
10
+ "BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ " +
11
+ "EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW " +
12
+ "GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY " +
13
+ "KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV " +
14
+ "MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY " +
15
+ "QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG " +
16
+ "TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS XK YE YT ZA ZM ZW"
17
+ ).split(" ");
18
+
19
+ const KNOWN = new Set(FLAG_CODES);
20
+
21
+ /** The code in capitals when a flag exists for it, else null. */
22
+ export function flagCode(code: string | null | undefined): string | null {
23
+ const upper = code?.trim().toUpperCase() ?? "";
24
+ return KNOWN.has(upper) ? upper : null;
25
+ }
@@ -0,0 +1,51 @@
1
+ <script lang="ts">
2
+ // =========================
3
+ // IMPORTS
4
+ // =========================
5
+ import { cn } from "../../lib/utils.js";
6
+ import { FLAG_BASE, flagCode } from "./codes.js";
7
+
8
+ // =========================
9
+ // PROPS
10
+ // =========================
11
+ interface Props {
12
+ /** ISO 3166-1 alpha-2 code, or XK for Kosovo, any case. Unknown or empty renders nothing. */
13
+ code: string | null | undefined;
14
+ /** Names the flag for a reader. Leave it out when the country name sits beside it. */
15
+ label?: string;
16
+ /** Where the host serves `<CODE>.svg`. */
17
+ base?: string;
18
+ class?: string;
19
+ }
20
+
21
+ let { code, label, base = FLAG_BASE, class: className }: Props = $props();
22
+
23
+ // =========================
24
+ // STATE
25
+ // =========================
26
+ let file = $derived(flagCode(code));
27
+ // A file the host doesn't serve hides the flag instead of showing a broken image.
28
+ let failedFor = $state<string | null>(null);
29
+ </script>
30
+
31
+ <!-- =========================== -->
32
+ <!-- MARKUP -->
33
+ <!-- =========================== -->
34
+ {#if file && failedFor !== file}
35
+ <img
36
+ src="{base}/{file}.svg"
37
+ alt={label ?? ""}
38
+ aria-hidden={label ? undefined : "true"}
39
+ width="3"
40
+ height="2"
41
+ loading="lazy"
42
+ decoding="async"
43
+ draggable="false"
44
+ onerror={() => (failedFor = file)}
45
+ class={cn(
46
+ "inline-block aspect-[3/2] h-[1em] w-auto max-w-none shrink-0 object-cover align-[-0.125em]",
47
+ "rounded-[min(var(--radius-sm),0.15em)] outline outline-1 -outline-offset-1 outline-black/10 dark:outline-white/15",
48
+ className,
49
+ )}
50
+ />
51
+ {/if}
@@ -0,0 +1,12 @@
1
+ interface Props {
2
+ /** ISO 3166-1 alpha-2 code, or XK for Kosovo, any case. Unknown or empty renders nothing. */
3
+ code: string | null | undefined;
4
+ /** Names the flag for a reader. Leave it out when the country name sits beside it. */
5
+ label?: string;
6
+ /** Where the host serves `<CODE>.svg`. */
7
+ base?: string;
8
+ class?: string;
9
+ }
10
+ declare const $$Component: import("svelte").Component<Props, {}, "">;
11
+ type $$Component = ReturnType<typeof $$Component>;
12
+ export default $$Component;
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#fedd00" d="M0 0h513v342H0z"/><path fill="#10069f" d="M0 0h171v342H0z"/><path fill="#d50032" d="M342 0h171v342H342z"/><path fill="#D80027" d="M198.9 113.6H256v64.8h-57.1z"/><g stroke="#D80027" stroke-width="7"><path d="M267.6 113.6v64.7M284.2 113.6v64.7M300.8 113.6v64.7"/></g><g stroke="#D80027" stroke-width="5"><path d="M247.4 178.4v64.7M234.1 178.4v64.7M220.8 178.5v53.9M207.1 178.5v47.1"/></g><path fill="#FFDA44" d="m199.9 146 27.6-32.4 28.5 32.3-28.1 32.4z"/><path fill="#BC8B00" d="M182.2 95.9v92.2c0 34.3 27.3 54.2 48.6 64.5-.8 1.4 25.1 8.3 25.1 8.3s25.9-6.9 25.1-8.3c21.4-10.3 48.6-30.1 48.6-64.5V95.9H182.2zm129.9 92.2c0 16.9-10 29.4-32.8 43.9-8.6 5.5-17.5 9-23.3 11-5.8-2-14.7-5.5-23.3-11-22.8-14.5-32.8-28-32.8-43.9v-74.5h112.2v74.5z"/><path fill="#D80027" d="M264.4 188.9h29.5v8.6h-29.5zM264.4 206.5h29.5v8.6h-29.5z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#FFF" d="M0 0h513v342H0z"/><path fill="#00843d" d="M0 0h513v114H0z"/><path d="M0 228h513v114H0z"/><path fill="#c8102e" d="M0 0h171v342H0z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#D32011" d="M0 0h513v342H0z"/><path fill="#007A36" d="M342 0h171v342H342z"/><path d="M0 0h171v342H0z"/><path fill="#FFF" d="M256.5 131.3c-17.5 0-31.8 14.3-31.8 31.8v47.7h63.6v-47.7c0-17.7-14.3-31.8-31.8-31.8z"/><path fill="#FFF" d="M256.5 93c-43.1 0-78 34.9-78 78s34.9 77.9 78 77.9 78-34.9 78-78-34.9-77.9-78-77.9zm0 142.4c-35.6 0-64.5-28.9-64.5-64.5s28.9-64.5 64.5-64.5 64.5 29 64.5 64.6c0 35.6-28.9 64.4-64.5 64.4z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path d="M0 0h513v342H0z"/><path fill="#0072c6" d="M88.8 136.5c-2.2 12.9-3.4 26.2-3.4 39.8 0 13.6 1.2 27 3.4 39.8L256 228.3l167.2-12.1c2.2-12.9 3.4-26.2 3.4-39.8s-1.2-27-3.4-39.8"/><path fill="#FFF" d="M423.2 219H88.8c15.8 69.8 84.7 122.3 167.2 122.3S407.4 288.8 423.2 219z"/><path fill="#FFDA44" d="M365.9 136.5H146.1l44.9-21.1-23.9-43.5 48.8 9.4L222 32l34 36.2L290 32l6.1 49.3 48.8-9.4-23.9 43.5z"/><g fill="#ce1126"><path d="M256.5 342 0 0v342z"/><path d="M513 342V0L256 342z"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#012169" d="M0 0h513v342H0z"/><path fill="#FFF" d="M454.8 265.38c7.94-10.93 13.24-24.27 13.24-40.42V104.89a59.754 59.754 0 0 1-36.02 12.01c-19.64 0-37.07-9.43-48.03-24.01-10.95 14.58-28.39 24.01-48.03 24.01-13.52 0-25.99-4.47-36.02-12v120.06c0 16.16 5.3 29.5 13.24 40.42H454.8z"/><path fill="#9cf" d="M310.23 260.98C332.65 296.96 384 309 384 309s51.35-12.04 73.77-48.02H310.23z"/><g fill="#f90"><path d="M396.66 172.21c.32 1.42 8.72 10.17 17.56 20.15 1.39 1.57-8.74 12.63-6.91 12.59 4.75-.12 19.27-17.26 19.34 6.65.05 15.94-30 27.51-30 27.51h17.47l.65 14.96s4.97-13.34 7.59-16.57c8.1-9.97 20.18-30.07 34.59-27.1 14.41 2.97-13.66-13.92-13.66-13.92s-5.2-15.19-16.63-16.97c-10.06-1.57-14.29-2.51-26.64-7.3-1.68-.65-3.88-2.32-3.36 0z"/><path d="M359.69 198.69c1.07-.99 4.46-12.63 8.69-25.28.67-1.99 15.31 1.27 14.35-.3-2.47-4.06-24.58-8.08-3.9-20.08 13.79-8 38.82 12.27 38.82 12.27l-8.72-15.14 12.64-8.03s-14.05 2.35-18.14 1.69c-12.68-2.04-36.13-2.48-40.75-16.45-4.62-13.97-5.25 18.79-5.25 18.79s-10.56 12.08-6.4 22.88c3.66 9.5 4.96 13.64 6.97 26.73.29 1.79-.06 4.53 1.69 2.92z"/><path d="M395.67 219.87c-1.39-.43-13.16 2.49-26.22 5.18-2.06.42-6.59-13.88-7.47-12.26-2.27 4.17 5.36 25.31-15.41 13.45-13.84-7.91-8.89-39.73-8.89-39.73l-8.71 15.14-13.29-6.9s9.09 10.96 10.57 14.84c4.6 11.99 16 32.49 6.24 43.49-9.76 11.01 18.88-4.9 18.88-4.9s15.76 3.07 23-5.96c6.38-7.94 9.3-11.14 19.61-19.45 1.43-1.12 3.97-2.19 1.69-2.9z"/></g><path fill="#012169" d="M0 0h256.5v171H0z"/><g fill="#FFF"><path d="M256.5 0v30.6l-45.3 25.2h45.3v59.4h-59.2l59.2 32.9V171h-26.7l-73.7-40.9V171h-55.7v-48.7L12.8 171H0v-30.6l45.3-25.2H0V55.8h59.2L0 22.9V0h26.7l73.7 40.9V0h55.7v48.7L243.7 0z"/><path d="M156.1 115.2 256.5 171v-15.8l-72-40zM100.4 55.8 0 0v15.8l72 40z"/></g><g fill="#D80027"><path d="M144.3 0h-32.1v69.5H0v32h112.2V171h32.1v-69.5h112.2v-32H144.3z"/><path d="M156.1 115.2 256.5 171v-15.8l-72-40zM72 115.2l-72 40V171l100.4-55.8zM100.4 55.8 0 0v15.8l72 40zM184.5 55.8l72-40V0L156.1 55.8z"/></g></svg>