sgiant-ai-widget 1.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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +155 -0
  3. package/dist/ai-invalidation.d.ts +29 -0
  4. package/dist/ai-invalidation.d.ts.map +1 -0
  5. package/dist/ai-invalidation.js +121 -0
  6. package/dist/ai-invalidation.js.map +1 -0
  7. package/dist/contrast.d.ts +87 -0
  8. package/dist/contrast.d.ts.map +1 -0
  9. package/dist/contrast.js +151 -0
  10. package/dist/contrast.js.map +1 -0
  11. package/dist/directive.d.ts +37 -0
  12. package/dist/directive.d.ts.map +1 -0
  13. package/dist/directive.js +87 -0
  14. package/dist/directive.js.map +1 -0
  15. package/dist/dom.d.ts +83 -0
  16. package/dist/dom.d.ts.map +1 -0
  17. package/dist/dom.js +146 -0
  18. package/dist/dom.js.map +1 -0
  19. package/dist/host-actions.d.ts +192 -0
  20. package/dist/host-actions.d.ts.map +1 -0
  21. package/dist/host-actions.js +443 -0
  22. package/dist/host-actions.js.map +1 -0
  23. package/dist/icons.d.ts +21 -0
  24. package/dist/icons.d.ts.map +1 -0
  25. package/dist/icons.js +84 -0
  26. package/dist/icons.js.map +1 -0
  27. package/dist/index.d.ts +600 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +4833 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/labels.d.ts +187 -0
  32. package/dist/labels.d.ts.map +1 -0
  33. package/dist/labels.js +232 -0
  34. package/dist/labels.js.map +1 -0
  35. package/dist/limits.d.ts +43 -0
  36. package/dist/limits.d.ts.map +1 -0
  37. package/dist/limits.js +48 -0
  38. package/dist/limits.js.map +1 -0
  39. package/dist/markdown.d.ts +23 -0
  40. package/dist/markdown.d.ts.map +1 -0
  41. package/dist/markdown.js +0 -0
  42. package/dist/markdown.js.map +1 -0
  43. package/dist/message-chrome.d.ts +45 -0
  44. package/dist/message-chrome.d.ts.map +1 -0
  45. package/dist/message-chrome.js +101 -0
  46. package/dist/message-chrome.js.map +1 -0
  47. package/dist/pane-follow.d.ts +67 -0
  48. package/dist/pane-follow.d.ts.map +1 -0
  49. package/dist/pane-follow.js +61 -0
  50. package/dist/pane-follow.js.map +1 -0
  51. package/dist/prefix.d.ts +9 -0
  52. package/dist/prefix.d.ts.map +1 -0
  53. package/dist/prefix.js +9 -0
  54. package/dist/prefix.js.map +1 -0
  55. package/dist/proposal-summary.d.ts +19 -0
  56. package/dist/proposal-summary.d.ts.map +1 -0
  57. package/dist/proposal-summary.js +73 -0
  58. package/dist/proposal-summary.js.map +1 -0
  59. package/dist/replay.d.ts +125 -0
  60. package/dist/replay.d.ts.map +1 -0
  61. package/dist/replay.js +172 -0
  62. package/dist/replay.js.map +1 -0
  63. package/dist/safe-url.d.ts +47 -0
  64. package/dist/safe-url.d.ts.map +1 -0
  65. package/dist/safe-url.js +80 -0
  66. package/dist/safe-url.js.map +1 -0
  67. package/dist/sgiant-ai-widget.global.js +778 -0
  68. package/dist/sgiant-ai-widget.global.js.map +7 -0
  69. package/dist/specs.d.ts +173 -0
  70. package/dist/specs.d.ts.map +1 -0
  71. package/dist/specs.js +231 -0
  72. package/dist/specs.js.map +1 -0
  73. package/dist/storage.d.ts +77 -0
  74. package/dist/storage.d.ts.map +1 -0
  75. package/dist/storage.js +124 -0
  76. package/dist/storage.js.map +1 -0
  77. package/dist/styles.d.ts +29 -0
  78. package/dist/styles.d.ts.map +1 -0
  79. package/dist/styles.js +876 -0
  80. package/dist/styles.js.map +1 -0
  81. package/dist/ui-control.d.ts +8 -0
  82. package/dist/ui-control.d.ts.map +1 -0
  83. package/dist/ui-control.js +8 -0
  84. package/dist/ui-control.js.map +1 -0
  85. package/dist/ui-render.d.ts +75 -0
  86. package/dist/ui-render.d.ts.map +1 -0
  87. package/dist/ui-render.js +617 -0
  88. package/dist/ui-render.js.map +1 -0
  89. package/dist/ui-spec.d.ts +143 -0
  90. package/dist/ui-spec.d.ts.map +1 -0
  91. package/dist/ui-spec.js +288 -0
  92. package/dist/ui-spec.js.map +1 -0
  93. package/package.json +61 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vedat Aydın Uğur
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # sgiant-ai-widget
2
+
3
+ A framework-agnostic, embeddable **AI chat widget**. One call mounts a floating
4
+ bubble and a streaming chat panel into any page.
5
+
6
+ No React. No framework. Vanilla DOM, themeable through CSS custom properties,
7
+ and one runtime dependency.
8
+
9
+ ```bash
10
+ npm install sgiant-ai-widget
11
+ ```
12
+
13
+ **[Live example →](https://vedatugur.github.io/sgiant-ai-widget/)** — a plain
14
+ HTML page with no bundler and no build step.
15
+
16
+ ## Two ways in
17
+
18
+ ### As a module (React, Next, Vue, Svelte, plain ESM)
19
+
20
+ ```ts
21
+ import { createAiChatWidget } from "sgiant-ai-widget";
22
+
23
+ const chat = createAiChatWidget({
24
+ endpoint: "https://api.example.com/chat",
25
+ title: "Aria",
26
+ greeting: "Hi! Ask me anything.",
27
+ accent: "#6d28d9",
28
+ });
29
+ chat.open();
30
+ ```
31
+
32
+ ### As a `<script>` tag, no build step
33
+
34
+ ```html
35
+ <script src="https://unpkg.com/sgiant-ai-widget/dist/sgiant-ai-widget.global.js"></script>
36
+ <script>
37
+ SgiantAiWidget.createAiChatWidget({ endpoint: "/chat" });
38
+ </script>
39
+ ```
40
+
41
+ The global build bundles its one dependency, so it is a single file. If your
42
+ page already uses a bundler, prefer the module build — it shares that dependency
43
+ instead of carrying a second copy.
44
+
45
+ ## The backend
46
+
47
+ One POST endpoint that streams newline-delimited JSON. Four frame types matter:
48
+
49
+ | frame | meaning |
50
+ | --- | --- |
51
+ | `{"type":"text","d":"…"}` | append this text |
52
+ | `{"threadId":"…"}` | name the conversation |
53
+ | `{"type":"done"}` | the turn is over |
54
+ | `{"type":"error","message":"…"}` | the turn failed |
55
+
56
+ The full contract, including a dependency-free reference server short enough to
57
+ read in one go, is in **[BACKEND.md](./BACKEND.md)**.
58
+
59
+ ## Theming
60
+
61
+ The entire stylesheet reads from `--aiw-*` CSS custom properties set on the
62
+ widget's roots. `accent` / `gradient` are the shorthand; the `theme` option
63
+ overrides any token — or theme from plain CSS by targeting the roots.
64
+
65
+ ```ts
66
+ createAiChatWidget({
67
+ // …
68
+ accent: "#6d28d9",
69
+ theme: {
70
+ // dark mode
71
+ surface: "#16161e",
72
+ "surface-2": "#22222c",
73
+ bg: "#101016",
74
+ text: "#e7e7ec",
75
+ "text-2": "#b6b6c2",
76
+ muted: "#8a8a96",
77
+ border: "#2b2b36",
78
+ "border-strong": "#3a3a48",
79
+ "border-soft": "#22222c",
80
+ },
81
+ });
82
+ ```
83
+
84
+ Tokens: `accent`, `accent-contrast`, `gradient`, `surface`, `surface-2`, `bg`,
85
+ `text`, `text-2`, `muted`, `border`, `border-strong`, `border-soft`.
86
+
87
+ ## Custom renderers (extension point)
88
+
89
+ The assistant can emit `[[<tag>:{json}]]` directives; built-ins cover
90
+ `widget` (stat/kpis/list/table), `preview` (sandboxed HTML), `navigate`,
91
+ `action`, `chips`, and `form`. Register your own tags to render anything else —
92
+ the widget strips the directive, mounts a host element in the log, and hands
93
+ you the parsed JSON:
94
+
95
+ ```ts
96
+ const chat = createAiChatWidget({
97
+ // …
98
+ renderers: {
99
+ calendar: (host, spec) => {
100
+ host.textContent = renderMyCalendar(spec); // validate spec yourself
101
+ return () => cleanup(); // optional disposer (message cleared)
102
+ },
103
+ },
104
+ });
105
+ // or later:
106
+ chat.registerRenderer("map", (host, spec) => drawMap(host, spec));
107
+ ```
108
+
109
+ Built-in tags are reserved (a plugin can't hijack the confirm-gated
110
+ `action`/`form` security model). A throwing renderer removes its host card
111
+ instead of breaking the message.
112
+
113
+ ## Charts without React
114
+
115
+ In-app hosts wire `renderDataWidget` (real `@sgiant/ui` charts). A standalone
116
+ embed has no React, so `render_chart` frames degrade to a stat/table — unless
117
+ you plug `renderChartFallback(host, spec, rows, comparisonRows)` with any
118
+ lightweight chart lib.
119
+
120
+ ## Transport
121
+
122
+ POSTs `{ accountId, threadId, content }` and reads a streamed body, tolerating
123
+ both shapes the platform emits:
124
+
125
+ - SSE — `data: {"type":"assistant_delta","text":"…"}`
126
+ - NDJSON — `{"d":"…"}`
127
+
128
+ plus `{type:"thread",threadId}`, `{type:"error",message}`, `{type:"done"}`.
129
+
130
+ ## Security
131
+
132
+ The widget holds no long-lived secret — the host supplies a token (Clerk session
133
+ in-app, or a short-lived **embed token** for external sites). The backend's OBO
134
+ scoping is the real ceiling; this is presentation only. The public embed-token
135
+ endpoint is the next backend step (tracked with the managed-AI build).
136
+
137
+ ## External embedding
138
+
139
+ For drop-in `<script>` use on third-party sites, bundle this module to a single
140
+ IIFE/UMD file that exposes `window.SgiantChat.init(opts)` — not yet wired (needs
141
+ the embed-token endpoint first).
142
+
143
+ ## About the `#123` references in the source
144
+
145
+ The comments cite issue numbers from the private tracker this widget was built
146
+ in, where it lived inside a monorepo until 2026-09-02. They are kept because the
147
+ reasoning around them is worth more than the tidiness of removing it — a comment
148
+ explaining *why* a wire value must not change is useful even when you cannot
149
+ open the ticket it cites.
150
+
151
+ Nothing behind those numbers is needed to use, read, or modify this package.
152
+
153
+ ## Licence
154
+
155
+ MIT © Vedat Aydın Uğur
@@ -0,0 +1,29 @@
1
+ /**
2
+ * A cross-tab "the assistant changed something" bus.
3
+ *
4
+ * Deliberately knows NOTHING about what changed: it carries an account id and a
5
+ * list of opaque domain strings, and the host decides what they mean. sgiant's
6
+ * mapping from those strings to query keys lives in `@sgiant/ai-apply`, because
7
+ * it is a description of sgiant's product rather than of this widget.
8
+ */
9
+ export interface AiChangeEvent {
10
+ accountId: string;
11
+ domains?: string[];
12
+ }
13
+ /** Tell other tabs an AI change landed so they invalidate too. No-op where
14
+ * BroadcastChannel is unavailable (old browsers / SSR). */
15
+ export declare function broadcastAiChange(accountId: string, domains?: string[]): void;
16
+ /** Subscribe to cross-tab AI change events; returns an unsubscribe fn. */
17
+ export declare function subscribeAiChange(cb: (e: AiChangeEvent) => void): () => void;
18
+ export interface LiveSyncOptions {
19
+ /** The SSE endpoint, e.g. `${API_BASE}/accounts/:id/live`. */
20
+ url: string;
21
+ /** Fresh auth token per (re)connect. */
22
+ getToken: () => Promise<string | null>;
23
+ /** Called for each change event the server pushes. */
24
+ onChange: (accountId: string, domains?: string[]) => void;
25
+ }
26
+ /** Subscribe to the server's live-sync stream. Returns an unsubscribe fn. No-op
27
+ * where fetch/streams are unavailable (SSR). */
28
+ export declare function subscribeLiveSync(opts: LiveSyncOptions): () => void;
29
+ //# sourceMappingURL=ai-invalidation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-invalidation.d.ts","sourceRoot":"","sources":["../src/ai-invalidation.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;4DAC4D;AAC5D,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAsB7E;AAED,0EAA0E;AAC1E,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,IAAI,GAAG,MAAM,IAAI,CAgB5E;AAQD,MAAM,WAAW,eAAe;IAC9B,8DAA8D;IAC9D,GAAG,EAAE,MAAM,CAAC;IACZ,wCAAwC;IACxC,QAAQ,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACvC,sDAAsD;IACtD,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CAC3D;AAED;iDACiD;AACjD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe,GAAG,MAAM,IAAI,CA0DnE"}
@@ -0,0 +1,121 @@
1
+ /**
2
+ * A cross-tab "the assistant changed something" bus.
3
+ *
4
+ * Deliberately knows NOTHING about what changed: it carries an account id and a
5
+ * list of opaque domain strings, and the host decides what they mean. sgiant's
6
+ * mapping from those strings to query keys lives in `@sgiant/ai-apply`, because
7
+ * it is a description of sgiant's product rather than of this widget.
8
+ */
9
+ // Neutral, like the bridge's channel: this ships to people whose product is
10
+ // not sgiant. It is a WIRE value between tabs of one deployment, so a change
11
+ // only costs an old tab and a new tab not seeing each other until reload.
12
+ const AI_LIVE_CHANNEL = "ai-widget-live";
13
+ /** Tell other tabs an AI change landed so they invalidate too. No-op where
14
+ * BroadcastChannel is unavailable (old browsers / SSR). */
15
+ export function broadcastAiChange(accountId, domains) {
16
+ // SAME-TAB listeners (e.g. the brand info bar's live "updated by AI" status)
17
+ // get a synchronous window event — BroadcastChannel is meant for OTHER tabs and
18
+ // is the less reliable path within one document, so this is the primary signal
19
+ // for anything on the current page; BroadcastChannel below still covers tabs.
20
+ if (typeof window !== "undefined") {
21
+ try {
22
+ window.dispatchEvent(new CustomEvent(AI_LIVE_CHANNEL, { detail: { accountId, domains } }));
23
+ }
24
+ catch {
25
+ /* CustomEvent unavailable */
26
+ }
27
+ }
28
+ if (typeof BroadcastChannel === "undefined")
29
+ return;
30
+ try {
31
+ const ch = new BroadcastChannel(AI_LIVE_CHANNEL);
32
+ ch.postMessage({ accountId, domains });
33
+ ch.close();
34
+ }
35
+ catch {
36
+ /* channel unavailable */
37
+ }
38
+ }
39
+ /** Subscribe to cross-tab AI change events; returns an unsubscribe fn. */
40
+ export function subscribeAiChange(cb) {
41
+ if (typeof BroadcastChannel === "undefined")
42
+ return () => { };
43
+ const ch = new BroadcastChannel(AI_LIVE_CHANNEL);
44
+ const handler = (ev) => {
45
+ const d = ev.data;
46
+ if (d && typeof d.accountId === "string")
47
+ cb({
48
+ accountId: d.accountId,
49
+ domains: Array.isArray(d.domains) ? d.domains : undefined,
50
+ });
51
+ };
52
+ ch.addEventListener("message", handler);
53
+ return () => {
54
+ ch.removeEventListener("message", handler);
55
+ ch.close();
56
+ };
57
+ }
58
+ /** Subscribe to the server's live-sync stream. Returns an unsubscribe fn. No-op
59
+ * where fetch/streams are unavailable (SSR). */
60
+ export function subscribeLiveSync(opts) {
61
+ if (typeof fetch === "undefined")
62
+ return () => { };
63
+ let closed = false;
64
+ let ctrl = null;
65
+ let backoff = 1000;
66
+ const connect = async () => {
67
+ if (closed)
68
+ return;
69
+ ctrl = new AbortController();
70
+ try {
71
+ const token = await opts.getToken();
72
+ const res = await fetch(opts.url, {
73
+ headers: token ? { authorization: `Bearer ${token}` } : {},
74
+ credentials: "include",
75
+ signal: ctrl.signal,
76
+ });
77
+ if (!res.ok || !res.body)
78
+ throw new Error(`live ${res.status}`);
79
+ backoff = 1000; // connected — reset backoff
80
+ const reader = res.body.getReader();
81
+ const dec = new TextDecoder();
82
+ let buf = "";
83
+ for (;;) {
84
+ const { value, done } = await reader.read();
85
+ if (done || closed)
86
+ break;
87
+ buf += dec.decode(value, { stream: true });
88
+ let nl;
89
+ while ((nl = buf.indexOf("\n\n")) >= 0) {
90
+ const frame = buf.slice(0, nl);
91
+ buf = buf.slice(nl + 2);
92
+ const line = frame.split("\n").find((l) => l.startsWith("data:"));
93
+ if (!line)
94
+ continue; // heartbeat / comment
95
+ try {
96
+ const d = JSON.parse(line.slice(5).trim());
97
+ if (d && typeof d.accountId === "string")
98
+ opts.onChange(d.accountId, Array.isArray(d.domains) ? d.domains : undefined);
99
+ }
100
+ catch {
101
+ /* non-json frame */
102
+ }
103
+ }
104
+ }
105
+ }
106
+ catch {
107
+ /* network error / abort */
108
+ }
109
+ if (closed)
110
+ return;
111
+ const wait = backoff;
112
+ backoff = Math.min(backoff * 2, 30000);
113
+ setTimeout(connect, wait);
114
+ };
115
+ void connect();
116
+ return () => {
117
+ closed = true;
118
+ ctrl?.abort();
119
+ };
120
+ }
121
+ //# sourceMappingURL=ai-invalidation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-invalidation.js","sourceRoot":"","sources":["../src/ai-invalidation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Accent/foreground contrast, WCAG 2.1 relative luminance (#307).
3
+ *
4
+ * WHY THIS EXISTS: the widget shipped `--aiw-accent-contrast: #fff`, correct
5
+ * against its own violet default (7.10:1) and never overridden by a single
6
+ * host. All three of ours pass a brand accent and none passes a contrast
7
+ * colour, so every accent-filled control rendered white on teal `#60C7C8` —
8
+ * **2.00:1**, against a 4.5:1 floor. Seventeen declarations, thirteen controls,
9
+ * four surfaces.
10
+ *
11
+ * A default cannot fix that, because the failing value IS the default. The
12
+ * pairing has to be DERIVED from whatever accent arrives — which also makes the
13
+ * widget safe to publish (#306), where we will not control the accent at all.
14
+ *
15
+ * No dependency on @sgiant/tokens on purpose: this file has to survive being
16
+ * extracted into a public package.
17
+ */
18
+ /** The two foregrounds we choose between. Both are brand values, and both are
19
+ * near enough to black/white to behave sensibly for an arbitrary accent a
20
+ * third-party host passes. */
21
+ export declare const CONTRAST_DARK = "#151D2F";
22
+ export declare const CONTRAST_LIGHT = "#FCF7E3";
23
+ /** WCAG 2.1 relative luminance. */
24
+ export declare function relativeLuminance(hex: string): number | null;
25
+ /** WCAG contrast ratio, 1–21. Null when either colour is unparseable. */
26
+ export declare function contrastRatio(a: string, b: string): number | null;
27
+ /**
28
+ * The foreground to use on a given accent: whichever of dark/light contrasts
29
+ * more. Returns null for anything it cannot parse — `color-mix()`, a CSS
30
+ * variable, `rebeccapurple` — so the caller leaves the host's own value alone
31
+ * instead of overwriting it with a guess.
32
+ *
33
+ * Measured on the accents this repo actually ships: teal → navy 8.41, amber →
34
+ * navy 8.73, orange → navy 5.98, and the violet default → cream 6.62. All four
35
+ * clear 4.5:1, which the host repo's contrast test asserts, so the
36
+ * next brand-colour change cannot quietly undo it.
37
+ */
38
+ export declare function resolveAccentContrast(accent: string): string | null;
39
+ /**
40
+ * Mix two hex colours in sRGB, the same way CSS `color-mix(in srgb, …)` does.
41
+ *
42
+ * Exists because the user's message bubble is not painted with the accent — it
43
+ * is painted with the accent mixed 76% into a near-black navy, and a foreground
44
+ * derived from the RAW accent is derived against the wrong background. Over the
45
+ * teal all three hosts pass, that bubble resolves to `#4a9d9e` and the sheet's
46
+ * literal `#fff` measures **3.18:1** on it: #307's defect, still live in the
47
+ * most-used element in the widget, because the fix was applied to the token and
48
+ * the bubble never read the token.
49
+ *
50
+ * The mix is computed HERE and handed to CSS as a resolved value, rather than
51
+ * computed here AND written as a `color-mix()` next door. Two implementations of
52
+ * one blend is how the foreground and the background drift apart, which is the
53
+ * whole bug again one level down.
54
+ *
55
+ * Null when either colour is unparseable — the caller must then leave whatever
56
+ * was there alone rather than guess, exactly as `resolveAccentContrast` does.
57
+ */
58
+ export declare function mixSrgb(a: string, b: string, ratioA: number): string | null;
59
+ /**
60
+ * The accent, adjusted until it is legible as TEXT on a given surface.
61
+ *
62
+ * `--aiw-accent` is a FILL colour. Twenty rules also use it as ink — links,
63
+ * hover states, the focused input, chips, KPI deltas, the menu icons, the
64
+ * active vote — and a fill is not an ink. Measured against the surfaces this
65
+ * sheet actually paints:
66
+ *
67
+ * teal #60C7C8 (what all three hosts pass) 2.00:1 on white, 9.05 on dark
68
+ * violet #6d28d9 (the widget's own default) 7.10:1 on white, 2.55 on dark
69
+ * amber #FBAA34 1.93:1 on white, 9.39 on dark
70
+ *
71
+ * So it is not "the teal is a bad accent". Every accent is illegible as ink on
72
+ * one of the two schemes; the widget's own default is the one that fails in the
73
+ * dark. Which scheme breaks depends only on where the accent sits between the
74
+ * two surfaces, and that is a property of the colour, not a mistake in it.
75
+ *
76
+ * The adjustment blends toward black or white — whichever the surface is
77
+ * further from — and returns the FIRST step that clears the floor, so the ink
78
+ * stays as close to the brand accent as legibility allows rather than jumping
79
+ * to a safe navy. Hue survives; some chroma does not.
80
+ *
81
+ * 5% steps because that is finer than the eye reads as a colour change and
82
+ * still terminates in twenty iterations. Null when the accent is unparseable —
83
+ * the caller leaves the token alone and the sheet's own default applies, which
84
+ * is the same contract as `resolveAccentContrast`.
85
+ */
86
+ export declare function accentInk(accent: string, surface: string, target?: number): string | null;
87
+ //# sourceMappingURL=contrast.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contrast.d.ts","sourceRoot":"","sources":["../src/contrast.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;+BAE+B;AAC/B,eAAO,MAAM,aAAa,YAAY,CAAC;AACvC,eAAO,MAAM,cAAc,YAAY,CAAC;AAqBxC,mCAAmC;AACnC,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQ5D;AAED,yEAAyE;AACzE,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKjE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKnE;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAU3E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,MAAM,SAAM,GACX,MAAM,GAAG,IAAI,CAWf"}
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Accent/foreground contrast, WCAG 2.1 relative luminance (#307).
3
+ *
4
+ * WHY THIS EXISTS: the widget shipped `--aiw-accent-contrast: #fff`, correct
5
+ * against its own violet default (7.10:1) and never overridden by a single
6
+ * host. All three of ours pass a brand accent and none passes a contrast
7
+ * colour, so every accent-filled control rendered white on teal `#60C7C8` —
8
+ * **2.00:1**, against a 4.5:1 floor. Seventeen declarations, thirteen controls,
9
+ * four surfaces.
10
+ *
11
+ * A default cannot fix that, because the failing value IS the default. The
12
+ * pairing has to be DERIVED from whatever accent arrives — which also makes the
13
+ * widget safe to publish (#306), where we will not control the accent at all.
14
+ *
15
+ * No dependency on @sgiant/tokens on purpose: this file has to survive being
16
+ * extracted into a public package.
17
+ */
18
+ /** The two foregrounds we choose between. Both are brand values, and both are
19
+ * near enough to black/white to behave sensibly for an arbitrary accent a
20
+ * third-party host passes. */
21
+ export const CONTRAST_DARK = "#151D2F"; // brand navy
22
+ export const CONTRAST_LIGHT = "#FCF7E3"; // brand cream
23
+ /** #rgb, #rrggbb, or #rrggbbaa → [r,g,b] 0-255. Null when unparseable — a
24
+ * caller must then keep whatever it already had rather than guess. */
25
+ function parseHex(hex) {
26
+ const h = hex.trim().replace(/^#/, "");
27
+ if (h.length === 3) {
28
+ const [r, g, b] = h.split("");
29
+ if (!/^[0-9a-f]{3}$/i.test(h))
30
+ return null;
31
+ return [parseInt(r + r, 16), parseInt(g + g, 16), parseInt(b + b, 16)];
32
+ }
33
+ if ((h.length === 6 || h.length === 8) && /^[0-9a-f]+$/i.test(h)) {
34
+ return [
35
+ parseInt(h.slice(0, 2), 16),
36
+ parseInt(h.slice(2, 4), 16),
37
+ parseInt(h.slice(4, 6), 16),
38
+ ];
39
+ }
40
+ return null;
41
+ }
42
+ /** WCAG 2.1 relative luminance. */
43
+ export function relativeLuminance(hex) {
44
+ const rgb = parseHex(hex);
45
+ if (!rgb)
46
+ return null;
47
+ const [r, g, b] = rgb.map((c) => {
48
+ const s = c / 255;
49
+ return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
50
+ });
51
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
52
+ }
53
+ /** WCAG contrast ratio, 1–21. Null when either colour is unparseable. */
54
+ export function contrastRatio(a, b) {
55
+ const la = relativeLuminance(a);
56
+ const lb = relativeLuminance(b);
57
+ if (la === null || lb === null)
58
+ return null;
59
+ return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
60
+ }
61
+ /**
62
+ * The foreground to use on a given accent: whichever of dark/light contrasts
63
+ * more. Returns null for anything it cannot parse — `color-mix()`, a CSS
64
+ * variable, `rebeccapurple` — so the caller leaves the host's own value alone
65
+ * instead of overwriting it with a guess.
66
+ *
67
+ * Measured on the accents this repo actually ships: teal → navy 8.41, amber →
68
+ * navy 8.73, orange → navy 5.98, and the violet default → cream 6.62. All four
69
+ * clear 4.5:1, which the host repo's contrast test asserts, so the
70
+ * next brand-colour change cannot quietly undo it.
71
+ */
72
+ export function resolveAccentContrast(accent) {
73
+ const dark = contrastRatio(accent, CONTRAST_DARK);
74
+ const light = contrastRatio(accent, CONTRAST_LIGHT);
75
+ if (dark === null || light === null)
76
+ return null;
77
+ return dark >= light ? CONTRAST_DARK : CONTRAST_LIGHT;
78
+ }
79
+ /**
80
+ * Mix two hex colours in sRGB, the same way CSS `color-mix(in srgb, …)` does.
81
+ *
82
+ * Exists because the user's message bubble is not painted with the accent — it
83
+ * is painted with the accent mixed 76% into a near-black navy, and a foreground
84
+ * derived from the RAW accent is derived against the wrong background. Over the
85
+ * teal all three hosts pass, that bubble resolves to `#4a9d9e` and the sheet's
86
+ * literal `#fff` measures **3.18:1** on it: #307's defect, still live in the
87
+ * most-used element in the widget, because the fix was applied to the token and
88
+ * the bubble never read the token.
89
+ *
90
+ * The mix is computed HERE and handed to CSS as a resolved value, rather than
91
+ * computed here AND written as a `color-mix()` next door. Two implementations of
92
+ * one blend is how the foreground and the background drift apart, which is the
93
+ * whole bug again one level down.
94
+ *
95
+ * Null when either colour is unparseable — the caller must then leave whatever
96
+ * was there alone rather than guess, exactly as `resolveAccentContrast` does.
97
+ */
98
+ export function mixSrgb(a, b, ratioA) {
99
+ const pa = parseHex(a);
100
+ const pb = parseHex(b);
101
+ if (!pa || !pb)
102
+ return null;
103
+ const p = Math.min(1, Math.max(0, ratioA));
104
+ const chan = (i) => Math.round(pa[i] * p + pb[i] * (1 - p))
105
+ .toString(16)
106
+ .padStart(2, "0");
107
+ return `#${chan(0)}${chan(1)}${chan(2)}`;
108
+ }
109
+ /**
110
+ * The accent, adjusted until it is legible as TEXT on a given surface.
111
+ *
112
+ * `--aiw-accent` is a FILL colour. Twenty rules also use it as ink — links,
113
+ * hover states, the focused input, chips, KPI deltas, the menu icons, the
114
+ * active vote — and a fill is not an ink. Measured against the surfaces this
115
+ * sheet actually paints:
116
+ *
117
+ * teal #60C7C8 (what all three hosts pass) 2.00:1 on white, 9.05 on dark
118
+ * violet #6d28d9 (the widget's own default) 7.10:1 on white, 2.55 on dark
119
+ * amber #FBAA34 1.93:1 on white, 9.39 on dark
120
+ *
121
+ * So it is not "the teal is a bad accent". Every accent is illegible as ink on
122
+ * one of the two schemes; the widget's own default is the one that fails in the
123
+ * dark. Which scheme breaks depends only on where the accent sits between the
124
+ * two surfaces, and that is a property of the colour, not a mistake in it.
125
+ *
126
+ * The adjustment blends toward black or white — whichever the surface is
127
+ * further from — and returns the FIRST step that clears the floor, so the ink
128
+ * stays as close to the brand accent as legibility allows rather than jumping
129
+ * to a safe navy. Hue survives; some chroma does not.
130
+ *
131
+ * 5% steps because that is finer than the eye reads as a colour change and
132
+ * still terminates in twenty iterations. Null when the accent is unparseable —
133
+ * the caller leaves the token alone and the sheet's own default applies, which
134
+ * is the same contract as `resolveAccentContrast`.
135
+ */
136
+ export function accentInk(accent, surface, target = 4.5) {
137
+ const surfaceLum = relativeLuminance(surface);
138
+ if (surfaceLum === null || parseHex(accent) === null)
139
+ return null;
140
+ const pole = surfaceLum > 0.5 ? "#000000" : "#ffffff";
141
+ for (let p = 1; p >= 0; p -= 0.05) {
142
+ const candidate = mixSrgb(accent, pole, p);
143
+ if (!candidate)
144
+ return null;
145
+ const ratio = contrastRatio(candidate, surface);
146
+ if (ratio !== null && ratio >= target)
147
+ return candidate;
148
+ }
149
+ return pole;
150
+ }
151
+ //# sourceMappingURL=contrast.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contrast.js","sourceRoot":"","sources":["../src/contrast.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The `[[tag:{json}]]` directive extractor the chat widget runs over every
3
+ * assistant reply.
4
+ *
5
+ * Lives in its own module because it is pure and because getting it wrong is
6
+ * VISIBLE: whatever this function fails to consume is shown to the user as raw
7
+ * text in the middle of a sentence. That makes it worth pinning down with
8
+ * tests, which the widget itself (imperative DOM, no harness) cannot be.
9
+ */
10
+ export interface ParsedDirective<T> {
11
+ spec: T;
12
+ /** The message with this directive removed. */
13
+ stripped: string;
14
+ start: number;
15
+ /** Index just past everything consumed — the directive leaves no residue. */
16
+ end: number;
17
+ }
18
+ /**
19
+ * Pull the FIRST `[[tag:{json}]]` out of `text`.
20
+ *
21
+ * Forgiving by design, because the delimiters are the part a model gets wrong
22
+ * most often and every slip is shown to the user as raw punctuation:
23
+ *
24
+ * - the JSON object is BRACE-MATCHED, string- and escape-aware, so a `}` or
25
+ * `]]` inside a quoted caption cannot end it early;
26
+ * - 0–2 trailing `]` are consumed, so `…}]]`, `…}]` and `…}` all parse;
27
+ * - a surplus `}` before the brackets is consumed too. A model that closes one
28
+ * brace too many is common, and it used to leave `}]]` sitting in the reply:
29
+ * the matcher stopped at the REAL closing brace (correctly), then looked for
30
+ * `]`, found `}`, and gave up — so the card rendered perfectly and the user
31
+ * still saw `}]]` printed above it.
32
+ *
33
+ * Only ADJACENT residue is swallowed. Once a character is neither `}` nor `]`,
34
+ * consumption stops, so nothing the assistant actually wrote is eaten.
35
+ */
36
+ export declare function parseJsonDirective<T>(text: string, tag: string): ParsedDirective<T> | null;
37
+ //# sourceMappingURL=directive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directive.d.ts","sourceRoot":"","sources":["../src/directive.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC,IAAI,EAAE,CAAC,CAAC;IACR,+CAA+C;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAClC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,MAAM,GACV,eAAe,CAAC,CAAC,CAAC,GAAG,IAAI,CA8C3B"}