@yak-io/javascript 0.5.0 → 0.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.
- package/dist/embed.d.ts +97 -0
- package/dist/embed.d.ts.map +1 -0
- package/dist/embed.js +564 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/package.json +1 -1
package/dist/embed.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { YakClient, type YakClientConfig } from "./client.js";
|
|
2
|
+
export type TriggerButtonConfig = {
|
|
3
|
+
/** Label displayed on the trigger button. Default: "Ask with AI" */
|
|
4
|
+
label?: string;
|
|
5
|
+
/** Custom color overrides for light mode */
|
|
6
|
+
lightButton?: {
|
|
7
|
+
background?: string;
|
|
8
|
+
color?: string;
|
|
9
|
+
border?: string;
|
|
10
|
+
};
|
|
11
|
+
/** Custom color overrides for dark mode */
|
|
12
|
+
darkButton?: {
|
|
13
|
+
background?: string;
|
|
14
|
+
color?: string;
|
|
15
|
+
border?: string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export type YakEmbedConfig = YakClientConfig & {
|
|
19
|
+
/** DOM element to append the widget into. Defaults to document.body. */
|
|
20
|
+
target?: HTMLElement;
|
|
21
|
+
/** Show the floating trigger button. Default: true */
|
|
22
|
+
trigger?: boolean | TriggerButtonConfig;
|
|
23
|
+
};
|
|
24
|
+
export type YakEmbedState = {
|
|
25
|
+
isOpen: boolean;
|
|
26
|
+
isReady: boolean;
|
|
27
|
+
isExpanded: boolean;
|
|
28
|
+
};
|
|
29
|
+
export type YakEmbedStateListener = (state: YakEmbedState) => void;
|
|
30
|
+
/**
|
|
31
|
+
* Drop-in widget that renders the yak chat iframe + optional trigger button.
|
|
32
|
+
* Wraps YakClient with DOM rendering, lazy iframe mounting, and consistent styling.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* const embed = new YakEmbed({
|
|
37
|
+
* appId: "my-app",
|
|
38
|
+
* theme: { position: "bottom-right" },
|
|
39
|
+
* onToolCall: async (name, args) => { ... },
|
|
40
|
+
* });
|
|
41
|
+
* embed.mount();
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare class YakEmbed {
|
|
45
|
+
private readonly client;
|
|
46
|
+
private readonly config;
|
|
47
|
+
private styleEl;
|
|
48
|
+
private panelRoot;
|
|
49
|
+
private container;
|
|
50
|
+
private iframe;
|
|
51
|
+
private triggerButton;
|
|
52
|
+
private isOpen;
|
|
53
|
+
private isReady;
|
|
54
|
+
private isExpanded;
|
|
55
|
+
private hasBeenOpened;
|
|
56
|
+
private pendingPrompt;
|
|
57
|
+
private mounted;
|
|
58
|
+
private stateListeners;
|
|
59
|
+
private mobileQuery;
|
|
60
|
+
private mobileHandler;
|
|
61
|
+
private expandHandler;
|
|
62
|
+
constructor(config: YakEmbedConfig);
|
|
63
|
+
/** The underlying headless YakClient for advanced usage */
|
|
64
|
+
getClient(): YakClient;
|
|
65
|
+
/**
|
|
66
|
+
* Mount the widget into the DOM. Call once after construction.
|
|
67
|
+
* Inserts styles and trigger button (if enabled). The iframe is lazily
|
|
68
|
+
* created on the first call to open().
|
|
69
|
+
*/
|
|
70
|
+
mount(target?: HTMLElement): void;
|
|
71
|
+
/** Remove all DOM elements and event listeners. */
|
|
72
|
+
destroy(): void;
|
|
73
|
+
/** Open the chat widget. Creates the iframe on first call (lazy mount). */
|
|
74
|
+
open(): void;
|
|
75
|
+
/** Close the chat widget. The iframe remains in the DOM for instant re-open. */
|
|
76
|
+
close(): void;
|
|
77
|
+
/** Toggle the chat widget open/closed. */
|
|
78
|
+
toggle(): void;
|
|
79
|
+
/** Open the chat and immediately send a prompt. */
|
|
80
|
+
openWithPrompt(prompt: string): void;
|
|
81
|
+
/** Get the current widget state. */
|
|
82
|
+
getState(): YakEmbedState;
|
|
83
|
+
/** Subscribe to state changes. Returns an unsubscribe function. */
|
|
84
|
+
onStateChange(listener: YakEmbedStateListener): () => void;
|
|
85
|
+
private createPanel;
|
|
86
|
+
private createTrigger;
|
|
87
|
+
private buildTriggerClasses;
|
|
88
|
+
private applyTriggerCustomColors;
|
|
89
|
+
private updatePanelState;
|
|
90
|
+
private updateTriggerState;
|
|
91
|
+
private sendPendingPrompt;
|
|
92
|
+
private sendFocusIfOpen;
|
|
93
|
+
private notifyMobileState;
|
|
94
|
+
private notifyIframeFullscreen;
|
|
95
|
+
private notifyListeners;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=embed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../src/embed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAM9D,MAAM,MAAM,mBAAmB,GAAG;IAChC,oEAAoE;IACpE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,WAAW,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACvE,2CAA2C;IAC3C,UAAU,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACvE,CAAC;AAIF,MAAM,MAAM,cAAc,GAAG,eAAe,GAAG;IAC7C,wEAAwE;IACxE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,sDAAsD;IACtD,OAAO,CAAC,EAAE,OAAO,GAAG,mBAAmB,CAAC;CACzC,CAAC;AAIF,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;AAgNnE;;;;;;;;;;;;;GAaG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IAGxC,OAAO,CAAC,OAAO,CAAiC;IAChD,OAAO,CAAC,SAAS,CAA+B;IAChD,OAAO,CAAC,SAAS,CAA+B;IAChD,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,aAAa,CAAkC;IAGvD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,OAAO,CAAS;IAGxB,OAAO,CAAC,cAAc,CAAoC;IAC1D,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,aAAa,CAAmD;IACxE,OAAO,CAAC,aAAa,CAA4C;gBAErD,MAAM,EAAE,cAAc;IAsBlC,2DAA2D;IACpD,SAAS,IAAI,SAAS;IAM7B;;;;OAIG;IACI,KAAK,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI;IA8BxC,mDAAmD;IAC5C,OAAO,IAAI,IAAI;IAoCtB,2EAA2E;IACpE,IAAI,IAAI,IAAI;IAkBnB,gFAAgF;IACzE,KAAK,IAAI,IAAI;IAQpB,0CAA0C;IACnC,MAAM,IAAI,IAAI;IAQrB,mDAAmD;IAC5C,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAM3C,oCAAoC;IAC7B,QAAQ,IAAI,aAAa;IAIhC,mEAAmE;IAC5D,aAAa,CAAC,QAAQ,EAAE,qBAAqB,GAAG,MAAM,IAAI;IASjE,OAAO,CAAC,WAAW;IA2CnB,OAAO,CAAC,aAAa;IAyCrB,OAAO,CAAC,mBAAmB;IAuB3B,OAAO,CAAC,wBAAwB;IA2BhC,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,kBAAkB;IA2B1B,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,sBAAsB;IAS9B,OAAO,CAAC,eAAe;CAUxB"}
|
package/dist/embed.js
ADDED
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import { YakClient } from "./client.js";
|
|
2
|
+
import { logger } from "./logger.js";
|
|
3
|
+
// ── CSS ─────────────────────────────────────────────────────────────────────
|
|
4
|
+
function getPanelStyles() {
|
|
5
|
+
return `
|
|
6
|
+
.yak-panel-root {
|
|
7
|
+
position: fixed;
|
|
8
|
+
top: 0; left: 0; right: 0; bottom: 0;
|
|
9
|
+
width: 100vw; height: 100vh;
|
|
10
|
+
pointer-events: none;
|
|
11
|
+
z-index: 9998;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.yak-panel-container {
|
|
15
|
+
position: absolute;
|
|
16
|
+
width: 500px; height: 600px;
|
|
17
|
+
max-width: calc(100vw - 40px);
|
|
18
|
+
max-height: calc(100vh - 120px);
|
|
19
|
+
border-radius: 15px;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
background-color: transparent;
|
|
22
|
+
pointer-events: auto;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.yak-panel-container[data-position="top-left"]:not(.yak-panel-drawer) { top: 16px; left: 16px; }
|
|
26
|
+
.yak-panel-container[data-position="top-center"]:not(.yak-panel-drawer) { top: 16px; left: 50%; transform: translateX(-50%); }
|
|
27
|
+
.yak-panel-container[data-position="top-right"]:not(.yak-panel-drawer) { top: 16px; right: 16px; }
|
|
28
|
+
.yak-panel-container[data-position="left-center"]:not(.yak-panel-drawer) { top: 50%; left: 16px; transform: translateY(-50%); }
|
|
29
|
+
.yak-panel-container[data-position="right-center"]:not(.yak-panel-drawer) { top: 50%; right: 16px; transform: translateY(-50%); }
|
|
30
|
+
.yak-panel-container[data-position="bottom-left"]:not(.yak-panel-drawer) { bottom: 16px; left: 16px; }
|
|
31
|
+
.yak-panel-container[data-position="bottom-center"]:not(.yak-panel-drawer) { bottom: 16px; left: 50%; transform: translateX(-50%); }
|
|
32
|
+
.yak-panel-container[data-position="bottom-right"]:not(.yak-panel-drawer) { bottom: 16px; right: 16px; }
|
|
33
|
+
|
|
34
|
+
.yak-panel-container:not(.yak-panel-drawer) { display: none; }
|
|
35
|
+
.yak-panel-container:not(.yak-panel-drawer)[data-open="true"] { display: block; }
|
|
36
|
+
|
|
37
|
+
.yak-panel-container[data-expanded="true"] {
|
|
38
|
+
width: calc(100vw - 32px) !important;
|
|
39
|
+
height: calc(100vh - 32px) !important;
|
|
40
|
+
max-width: none !important; max-height: none !important;
|
|
41
|
+
top: 16px !important; left: 16px !important; right: 16px !important; bottom: 16px !important;
|
|
42
|
+
border-radius: 15px !important;
|
|
43
|
+
border: 1px solid rgba(0, 0, 0, 0.1) !important;
|
|
44
|
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15) !important;
|
|
45
|
+
}
|
|
46
|
+
@media (prefers-color-scheme: dark) {
|
|
47
|
+
.yak-panel-container[data-expanded="true"]:not(.yak-panel-light) { border-color: rgba(255,255,255,0.1) !important; }
|
|
48
|
+
}
|
|
49
|
+
.yak-panel-container.yak-panel-dark[data-expanded="true"] { border-color: rgba(255,255,255,0.1) !important; }
|
|
50
|
+
.yak-panel-container.yak-panel-light[data-expanded="true"] { border-color: rgba(0,0,0,0.1) !important; }
|
|
51
|
+
|
|
52
|
+
.yak-panel-container.yak-panel-drawer {
|
|
53
|
+
height: calc(100% - 32px); max-width: 100vw; max-height: none;
|
|
54
|
+
border-radius: 15px;
|
|
55
|
+
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
|
56
|
+
}
|
|
57
|
+
.yak-panel-container.yak-panel-drawer[data-position="left-center"],
|
|
58
|
+
.yak-panel-container.yak-panel-drawer[data-position="top-left"],
|
|
59
|
+
.yak-panel-container.yak-panel-drawer[data-position="bottom-left"] {
|
|
60
|
+
top: 16px; left: 16px; bottom: 16px;
|
|
61
|
+
transform: translateX(calc(-100% - 16px));
|
|
62
|
+
}
|
|
63
|
+
.yak-panel-container.yak-panel-drawer[data-position="right-center"],
|
|
64
|
+
.yak-panel-container.yak-panel-drawer[data-position="top-right"],
|
|
65
|
+
.yak-panel-container.yak-panel-drawer[data-position="bottom-right"] {
|
|
66
|
+
top: 16px; right: 16px; bottom: 16px;
|
|
67
|
+
transform: translateX(calc(100% + 16px));
|
|
68
|
+
}
|
|
69
|
+
.yak-panel-container.yak-panel-drawer[data-position="top-center"],
|
|
70
|
+
.yak-panel-container.yak-panel-drawer[data-position="bottom-center"] {
|
|
71
|
+
top: 16px; right: 16px; bottom: 16px;
|
|
72
|
+
transform: translateX(calc(100% + 16px));
|
|
73
|
+
}
|
|
74
|
+
.yak-panel-container.yak-panel-drawer[data-open="true"] { transform: translateX(0); }
|
|
75
|
+
|
|
76
|
+
.yak-panel-iframe {
|
|
77
|
+
position: absolute; inset: 0;
|
|
78
|
+
width: 100%; height: 100%; border: none;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@media (max-width: 640px) {
|
|
82
|
+
.yak-panel-container:not(.yak-panel-drawer) {
|
|
83
|
+
width: 100% !important; height: 100% !important; height: 100dvh !important;
|
|
84
|
+
max-width: none !important; max-height: none !important;
|
|
85
|
+
top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important;
|
|
86
|
+
border-radius: 0 !important;
|
|
87
|
+
}
|
|
88
|
+
.yak-panel-container.yak-panel-drawer { width: 100% !important; max-width: none !important; }
|
|
89
|
+
}
|
|
90
|
+
`;
|
|
91
|
+
}
|
|
92
|
+
function getTriggerStyles() {
|
|
93
|
+
return `
|
|
94
|
+
.yak-widget-trigger {
|
|
95
|
+
position: fixed; z-index: 9997;
|
|
96
|
+
display: flex; align-items: center; gap: 12px;
|
|
97
|
+
border: none; border-radius: 30px;
|
|
98
|
+
padding: 0 5px 0 20px; height: 45px; min-width: 45px; width: auto;
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
|
101
|
+
overflow: hidden;
|
|
102
|
+
background-color: #000; color: #fff;
|
|
103
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
104
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.yak-widget-trigger[data-position="top-left"] { top: 28px; left: 28px; flex-direction: row-reverse; }
|
|
108
|
+
.yak-widget-trigger[data-position="top-center"] { top: 28px; left: 50%; transform: translateX(-50%); }
|
|
109
|
+
.yak-widget-trigger[data-position="top-right"] { top: 28px; right: 28px; }
|
|
110
|
+
.yak-widget-trigger[data-position="left-center"] { top: 50%; left: 28px; transform: translateY(-50%); flex-direction: row-reverse; }
|
|
111
|
+
.yak-widget-trigger[data-position="right-center"] { top: 50%; right: 28px; transform: translateY(-50%); }
|
|
112
|
+
.yak-widget-trigger[data-position="bottom-left"] { bottom: 28px; left: 28px; flex-direction: row-reverse; }
|
|
113
|
+
.yak-widget-trigger[data-position="bottom-center"] { bottom: 28px; left: 50%; transform: translateX(-50%); }
|
|
114
|
+
.yak-widget-trigger[data-position="bottom-right"] { bottom: 28px; right: 28px; }
|
|
115
|
+
|
|
116
|
+
.yak-widget-trigger-label { font-size: 14px; font-weight: 600; white-space: nowrap; }
|
|
117
|
+
|
|
118
|
+
.yak-widget-icon-bg {
|
|
119
|
+
display: flex; align-items: center; justify-content: center;
|
|
120
|
+
width: 36px; height: 36px; border-radius: 50%;
|
|
121
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.yak-widget-icon { width: 20px; height: 20px; color: currentColor; }
|
|
125
|
+
|
|
126
|
+
@media (prefers-color-scheme: dark) {
|
|
127
|
+
.yak-widget-trigger:not(.yak-widget-light) .yak-widget-icon { filter: invert(1); }
|
|
128
|
+
}
|
|
129
|
+
.yak-widget-trigger.yak-widget-dark .yak-widget-icon { filter: invert(1); }
|
|
130
|
+
.yak-widget-trigger.yak-widget-light .yak-widget-icon { filter: none; }
|
|
131
|
+
|
|
132
|
+
.yak-widget-spinner {
|
|
133
|
+
width: 20px; height: 20px;
|
|
134
|
+
border: 2px solid currentColor; border-top-color: transparent; border-radius: 50%;
|
|
135
|
+
animation: yak-widget-spin 0.8s linear infinite;
|
|
136
|
+
}
|
|
137
|
+
@keyframes yak-widget-spin { to { transform: rotate(360deg); } }
|
|
138
|
+
|
|
139
|
+
.yak-widget-trigger:disabled { cursor: wait; }
|
|
140
|
+
|
|
141
|
+
.yak-widget-trigger.yak-widget-custom-light {
|
|
142
|
+
background-color: var(--yak-btn-light-bg, #fff); color: var(--yak-btn-light-color, #000);
|
|
143
|
+
border: 1px solid var(--yak-btn-light-border, transparent);
|
|
144
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
145
|
+
}
|
|
146
|
+
.yak-widget-trigger.yak-widget-custom-dark {
|
|
147
|
+
background-color: var(--yak-btn-dark-bg, #000); color: var(--yak-btn-dark-color, #fff);
|
|
148
|
+
border: 1px solid var(--yak-btn-dark-border, transparent);
|
|
149
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@media (prefers-color-scheme: light) {
|
|
153
|
+
.yak-widget-trigger[data-has-light-custom]:not(.yak-widget-dark) {
|
|
154
|
+
background-color: var(--yak-btn-light-bg, #fff); color: var(--yak-btn-light-color, #000);
|
|
155
|
+
border: 1px solid var(--yak-btn-light-border, transparent);
|
|
156
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
@media (prefers-color-scheme: dark) {
|
|
160
|
+
.yak-widget-trigger[data-has-dark-custom]:not(.yak-widget-light) {
|
|
161
|
+
background-color: var(--yak-btn-dark-bg, #000); color: var(--yak-btn-dark-color, #fff);
|
|
162
|
+
border: 1px solid var(--yak-btn-dark-border, transparent);
|
|
163
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
@media (prefers-color-scheme: light) {
|
|
168
|
+
.yak-widget-trigger:not(.yak-widget-dark):not([data-has-light-custom]) {
|
|
169
|
+
background-color: #fff; color: #000;
|
|
170
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); border: 1px solid #e5e5e5;
|
|
171
|
+
}
|
|
172
|
+
.yak-widget-trigger:not(.yak-widget-dark):not([data-has-light-custom]) .yak-widget-icon-bg {
|
|
173
|
+
background-color: rgba(0, 0, 0, 0.05);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@media (prefers-color-scheme: dark) {
|
|
178
|
+
.yak-widget-trigger:not(.yak-widget-light):not([data-has-dark-custom]) {
|
|
179
|
+
background-color: #000; color: #fff;
|
|
180
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); border: none;
|
|
181
|
+
}
|
|
182
|
+
.yak-widget-trigger:not(.yak-widget-light):not([data-has-dark-custom]) .yak-widget-icon-bg {
|
|
183
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.yak-widget-trigger.yak-widget-light:not(.yak-widget-custom-light) {
|
|
188
|
+
background-color: #fff; color: #000;
|
|
189
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); border: 1px solid #e5e5e5;
|
|
190
|
+
}
|
|
191
|
+
.yak-widget-trigger.yak-widget-light:not(.yak-widget-custom-light) .yak-widget-icon-bg {
|
|
192
|
+
background-color: rgba(0, 0, 0, 0.05);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.yak-widget-trigger.yak-widget-dark:not(.yak-widget-custom-dark) {
|
|
196
|
+
background-color: #000; color: #fff;
|
|
197
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); border: none;
|
|
198
|
+
}
|
|
199
|
+
.yak-widget-trigger.yak-widget-dark:not(.yak-widget-custom-dark) .yak-widget-icon-bg {
|
|
200
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
201
|
+
}
|
|
202
|
+
`;
|
|
203
|
+
}
|
|
204
|
+
// ── YakEmbed class ──────────────────────────────────────────────────────────
|
|
205
|
+
/**
|
|
206
|
+
* Drop-in widget that renders the yak chat iframe + optional trigger button.
|
|
207
|
+
* Wraps YakClient with DOM rendering, lazy iframe mounting, and consistent styling.
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```ts
|
|
211
|
+
* const embed = new YakEmbed({
|
|
212
|
+
* appId: "my-app",
|
|
213
|
+
* theme: { position: "bottom-right" },
|
|
214
|
+
* onToolCall: async (name, args) => { ... },
|
|
215
|
+
* });
|
|
216
|
+
* embed.mount();
|
|
217
|
+
* ```
|
|
218
|
+
*/
|
|
219
|
+
export class YakEmbed {
|
|
220
|
+
client;
|
|
221
|
+
config;
|
|
222
|
+
// DOM elements
|
|
223
|
+
styleEl = null;
|
|
224
|
+
panelRoot = null;
|
|
225
|
+
container = null;
|
|
226
|
+
iframe = null;
|
|
227
|
+
triggerButton = null;
|
|
228
|
+
// State
|
|
229
|
+
isOpen = false;
|
|
230
|
+
isReady = false;
|
|
231
|
+
isExpanded = false;
|
|
232
|
+
hasBeenOpened = false;
|
|
233
|
+
pendingPrompt = null;
|
|
234
|
+
mounted = false;
|
|
235
|
+
// Listeners
|
|
236
|
+
stateListeners = new Set();
|
|
237
|
+
mobileQuery = null;
|
|
238
|
+
mobileHandler = null;
|
|
239
|
+
expandHandler = null;
|
|
240
|
+
constructor(config) {
|
|
241
|
+
this.config = config;
|
|
242
|
+
// Wrap callbacks to integrate with our state
|
|
243
|
+
this.client = new YakClient({
|
|
244
|
+
...config,
|
|
245
|
+
onReady: () => {
|
|
246
|
+
this.isReady = true;
|
|
247
|
+
this.updatePanelState();
|
|
248
|
+
this.updateTriggerState();
|
|
249
|
+
this.sendPendingPrompt();
|
|
250
|
+
this.sendFocusIfOpen();
|
|
251
|
+
this.notifyMobileState();
|
|
252
|
+
config.onReady?.();
|
|
253
|
+
},
|
|
254
|
+
onClose: () => {
|
|
255
|
+
this.close();
|
|
256
|
+
config.onClose?.();
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
/** The underlying headless YakClient for advanced usage */
|
|
261
|
+
getClient() {
|
|
262
|
+
return this.client;
|
|
263
|
+
}
|
|
264
|
+
// ── Lifecycle ───────────────────────────────────────────────────────────
|
|
265
|
+
/**
|
|
266
|
+
* Mount the widget into the DOM. Call once after construction.
|
|
267
|
+
* Inserts styles and trigger button (if enabled). The iframe is lazily
|
|
268
|
+
* created on the first call to open().
|
|
269
|
+
*/
|
|
270
|
+
mount(target) {
|
|
271
|
+
if (this.mounted)
|
|
272
|
+
return;
|
|
273
|
+
this.mounted = true;
|
|
274
|
+
const parent = target ?? this.config.target ?? document.body;
|
|
275
|
+
// Inject styles
|
|
276
|
+
this.styleEl = document.createElement("style");
|
|
277
|
+
this.styleEl.textContent = getPanelStyles() + getTriggerStyles();
|
|
278
|
+
parent.appendChild(this.styleEl);
|
|
279
|
+
// Create trigger button
|
|
280
|
+
if (this.config.trigger !== false) {
|
|
281
|
+
this.createTrigger(parent);
|
|
282
|
+
}
|
|
283
|
+
// Listen for expansion messages from iframe
|
|
284
|
+
this.expandHandler = (event) => {
|
|
285
|
+
if (event.data?.type === "YAK_SET_EXPANDED") {
|
|
286
|
+
this.isExpanded = Boolean(event.data.expanded);
|
|
287
|
+
this.updatePanelState();
|
|
288
|
+
this.notifyListeners();
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
window.addEventListener("message", this.expandHandler);
|
|
292
|
+
// Start the client's message listeners
|
|
293
|
+
this.client.mount();
|
|
294
|
+
}
|
|
295
|
+
/** Remove all DOM elements and event listeners. */
|
|
296
|
+
destroy() {
|
|
297
|
+
if (!this.mounted)
|
|
298
|
+
return;
|
|
299
|
+
this.mounted = false;
|
|
300
|
+
this.client.unmount();
|
|
301
|
+
if (this.expandHandler) {
|
|
302
|
+
window.removeEventListener("message", this.expandHandler);
|
|
303
|
+
this.expandHandler = null;
|
|
304
|
+
}
|
|
305
|
+
if (this.mobileQuery && this.mobileHandler) {
|
|
306
|
+
this.mobileQuery.removeEventListener("change", this.mobileHandler);
|
|
307
|
+
this.mobileQuery = null;
|
|
308
|
+
this.mobileHandler = null;
|
|
309
|
+
}
|
|
310
|
+
this.panelRoot?.remove();
|
|
311
|
+
this.triggerButton?.remove();
|
|
312
|
+
this.styleEl?.remove();
|
|
313
|
+
this.panelRoot = null;
|
|
314
|
+
this.container = null;
|
|
315
|
+
this.iframe = null;
|
|
316
|
+
this.triggerButton = null;
|
|
317
|
+
this.styleEl = null;
|
|
318
|
+
this.isOpen = false;
|
|
319
|
+
this.isReady = false;
|
|
320
|
+
this.isExpanded = false;
|
|
321
|
+
this.hasBeenOpened = false;
|
|
322
|
+
this.stateListeners.clear();
|
|
323
|
+
}
|
|
324
|
+
// ── Public API ──────────────────────────────────────────────────────────
|
|
325
|
+
/** Open the chat widget. Creates the iframe on first call (lazy mount). */
|
|
326
|
+
open() {
|
|
327
|
+
if (!this.mounted)
|
|
328
|
+
return;
|
|
329
|
+
if (!this.hasBeenOpened) {
|
|
330
|
+
this.hasBeenOpened = true;
|
|
331
|
+
const parent = this.config.target ?? document.body;
|
|
332
|
+
this.createPanel(parent);
|
|
333
|
+
}
|
|
334
|
+
this.isOpen = true;
|
|
335
|
+
this.client.setWidgetOpen(true);
|
|
336
|
+
this.updatePanelState();
|
|
337
|
+
this.updateTriggerState();
|
|
338
|
+
this.sendFocusIfOpen();
|
|
339
|
+
this.notifyListeners();
|
|
340
|
+
}
|
|
341
|
+
/** Close the chat widget. The iframe remains in the DOM for instant re-open. */
|
|
342
|
+
close() {
|
|
343
|
+
this.isOpen = false;
|
|
344
|
+
this.client.setWidgetOpen(false);
|
|
345
|
+
this.updatePanelState();
|
|
346
|
+
this.updateTriggerState();
|
|
347
|
+
this.notifyListeners();
|
|
348
|
+
}
|
|
349
|
+
/** Toggle the chat widget open/closed. */
|
|
350
|
+
toggle() {
|
|
351
|
+
if (this.isOpen) {
|
|
352
|
+
this.close();
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
this.open();
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/** Open the chat and immediately send a prompt. */
|
|
359
|
+
openWithPrompt(prompt) {
|
|
360
|
+
this.pendingPrompt = prompt;
|
|
361
|
+
this.open();
|
|
362
|
+
this.sendPendingPrompt();
|
|
363
|
+
}
|
|
364
|
+
/** Get the current widget state. */
|
|
365
|
+
getState() {
|
|
366
|
+
return { isOpen: this.isOpen, isReady: this.isReady, isExpanded: this.isExpanded };
|
|
367
|
+
}
|
|
368
|
+
/** Subscribe to state changes. Returns an unsubscribe function. */
|
|
369
|
+
onStateChange(listener) {
|
|
370
|
+
this.stateListeners.add(listener);
|
|
371
|
+
return () => {
|
|
372
|
+
this.stateListeners.delete(listener);
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
// ── DOM creation ────────────────────────────────────────────────────────
|
|
376
|
+
createPanel(parent) {
|
|
377
|
+
const theme = this.config.theme;
|
|
378
|
+
const position = theme?.position ?? "bottom-right";
|
|
379
|
+
const colorMode = theme?.colorMode;
|
|
380
|
+
const displayMode = theme?.displayMode ?? "chatbox";
|
|
381
|
+
const isDrawer = displayMode === "drawer";
|
|
382
|
+
// Root overlay (pointer-events: none)
|
|
383
|
+
this.panelRoot = document.createElement("div");
|
|
384
|
+
this.panelRoot.className = "yak-panel-root";
|
|
385
|
+
// Container
|
|
386
|
+
this.container = document.createElement("div");
|
|
387
|
+
const classes = ["yak-panel-container"];
|
|
388
|
+
if (isDrawer)
|
|
389
|
+
classes.push("yak-panel-drawer");
|
|
390
|
+
if (colorMode === "light")
|
|
391
|
+
classes.push("yak-panel-light");
|
|
392
|
+
else if (colorMode === "dark")
|
|
393
|
+
classes.push("yak-panel-dark");
|
|
394
|
+
this.container.className = classes.join(" ");
|
|
395
|
+
this.container.dataset.position = position;
|
|
396
|
+
// Iframe
|
|
397
|
+
this.iframe = document.createElement("iframe");
|
|
398
|
+
this.iframe.src = this.client.getEmbedUrl();
|
|
399
|
+
this.iframe.className = "yak-panel-iframe";
|
|
400
|
+
this.iframe.title = "yak-chat-host";
|
|
401
|
+
this.iframe.allow = "clipboard-write";
|
|
402
|
+
this.iframe.addEventListener("load", () => {
|
|
403
|
+
this.client.setIframeWindow(this.iframe?.contentWindow ?? null);
|
|
404
|
+
});
|
|
405
|
+
this.container.appendChild(this.iframe);
|
|
406
|
+
this.panelRoot.appendChild(this.container);
|
|
407
|
+
parent.appendChild(this.panelRoot);
|
|
408
|
+
// Set up mobile detection
|
|
409
|
+
this.mobileQuery = window.matchMedia("(max-width: 640px)");
|
|
410
|
+
this.mobileHandler = (e) => {
|
|
411
|
+
this.notifyIframeFullscreen(e.matches);
|
|
412
|
+
};
|
|
413
|
+
this.mobileQuery.addEventListener("change", this.mobileHandler);
|
|
414
|
+
}
|
|
415
|
+
createTrigger(parent) {
|
|
416
|
+
const theme = this.config.theme;
|
|
417
|
+
const position = theme?.position ?? "bottom-right";
|
|
418
|
+
const colorMode = theme?.colorMode;
|
|
419
|
+
const triggerConfig = typeof this.config.trigger === "object" ? this.config.trigger : {};
|
|
420
|
+
const label = triggerConfig.label ?? "Ask with AI";
|
|
421
|
+
this.triggerButton = document.createElement("button");
|
|
422
|
+
this.triggerButton.type = "button";
|
|
423
|
+
this.triggerButton.setAttribute("aria-label", "Open chat");
|
|
424
|
+
this.triggerButton.dataset.position = position;
|
|
425
|
+
this.triggerButton.className = this.buildTriggerClasses(colorMode, triggerConfig);
|
|
426
|
+
this.applyTriggerCustomColors(triggerConfig);
|
|
427
|
+
// Inner content
|
|
428
|
+
const labelEl = document.createElement("span");
|
|
429
|
+
labelEl.className = "yak-widget-trigger-label";
|
|
430
|
+
labelEl.textContent = label;
|
|
431
|
+
const iconBg = document.createElement("div");
|
|
432
|
+
iconBg.className = "yak-widget-icon-bg";
|
|
433
|
+
const logoImg = document.createElement("img");
|
|
434
|
+
logoImg.src = `${this.client.getIframeOrigin()}/logo.svg`;
|
|
435
|
+
logoImg.alt = "";
|
|
436
|
+
logoImg.width = 20;
|
|
437
|
+
logoImg.height = 20;
|
|
438
|
+
logoImg.className = "yak-widget-icon";
|
|
439
|
+
iconBg.appendChild(logoImg);
|
|
440
|
+
this.triggerButton.appendChild(labelEl);
|
|
441
|
+
this.triggerButton.appendChild(iconBg);
|
|
442
|
+
this.triggerButton.addEventListener("click", () => {
|
|
443
|
+
this.open();
|
|
444
|
+
});
|
|
445
|
+
parent.appendChild(this.triggerButton);
|
|
446
|
+
}
|
|
447
|
+
buildTriggerClasses(colorMode, triggerConfig) {
|
|
448
|
+
const classes = ["yak-widget-trigger"];
|
|
449
|
+
if (colorMode === "light")
|
|
450
|
+
classes.push("yak-widget-light");
|
|
451
|
+
else if (colorMode === "dark")
|
|
452
|
+
classes.push("yak-widget-dark");
|
|
453
|
+
const hasLightCustom = triggerConfig.lightButton?.background ||
|
|
454
|
+
triggerConfig.lightButton?.color ||
|
|
455
|
+
triggerConfig.lightButton?.border;
|
|
456
|
+
const hasDarkCustom = triggerConfig.darkButton?.background ||
|
|
457
|
+
triggerConfig.darkButton?.color ||
|
|
458
|
+
triggerConfig.darkButton?.border;
|
|
459
|
+
if (colorMode === "light" && hasLightCustom)
|
|
460
|
+
classes.push("yak-widget-custom-light");
|
|
461
|
+
else if (colorMode === "dark" && hasDarkCustom)
|
|
462
|
+
classes.push("yak-widget-custom-dark");
|
|
463
|
+
return classes.join(" ");
|
|
464
|
+
}
|
|
465
|
+
applyTriggerCustomColors(triggerConfig) {
|
|
466
|
+
if (!this.triggerButton)
|
|
467
|
+
return;
|
|
468
|
+
const { lightButton, darkButton } = triggerConfig;
|
|
469
|
+
const hasLightCustom = lightButton?.background || lightButton?.color || lightButton?.border;
|
|
470
|
+
const hasDarkCustom = darkButton?.background || darkButton?.color || darkButton?.border;
|
|
471
|
+
if (hasLightCustom || hasDarkCustom) {
|
|
472
|
+
const vars = [
|
|
473
|
+
["--yak-btn-light-bg", lightButton?.background],
|
|
474
|
+
["--yak-btn-light-color", lightButton?.color],
|
|
475
|
+
["--yak-btn-light-border", lightButton?.border],
|
|
476
|
+
["--yak-btn-dark-bg", darkButton?.background],
|
|
477
|
+
["--yak-btn-dark-color", darkButton?.color],
|
|
478
|
+
["--yak-btn-dark-border", darkButton?.border],
|
|
479
|
+
];
|
|
480
|
+
for (const [prop, value] of vars) {
|
|
481
|
+
if (value)
|
|
482
|
+
this.triggerButton.style.setProperty(prop, value);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (hasLightCustom)
|
|
486
|
+
this.triggerButton.dataset.hasLightCustom = "true";
|
|
487
|
+
if (hasDarkCustom)
|
|
488
|
+
this.triggerButton.dataset.hasDarkCustom = "true";
|
|
489
|
+
}
|
|
490
|
+
// ── Internal state management ───────────────────────────────────────────
|
|
491
|
+
updatePanelState() {
|
|
492
|
+
if (!this.container)
|
|
493
|
+
return;
|
|
494
|
+
this.container.dataset.open = String(this.isOpen && this.isReady);
|
|
495
|
+
this.container.dataset.expanded = String(this.isExpanded);
|
|
496
|
+
if (this.panelRoot) {
|
|
497
|
+
this.panelRoot.dataset.expanded = String(this.isExpanded);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
updateTriggerState() {
|
|
501
|
+
if (!this.triggerButton)
|
|
502
|
+
return;
|
|
503
|
+
const isLoading = this.isOpen && !this.isReady;
|
|
504
|
+
this.triggerButton.disabled = isLoading;
|
|
505
|
+
this.triggerButton.setAttribute("aria-label", isLoading ? "Loading chat" : "Open chat");
|
|
506
|
+
// Swap icon content: spinner vs logo
|
|
507
|
+
const iconBg = this.triggerButton.querySelector(".yak-widget-icon-bg");
|
|
508
|
+
if (!iconBg)
|
|
509
|
+
return;
|
|
510
|
+
if (isLoading) {
|
|
511
|
+
iconBg.innerHTML = '<div class="yak-widget-spinner" aria-hidden="true"></div>';
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
const existing = iconBg.querySelector(".yak-widget-icon");
|
|
515
|
+
if (!existing) {
|
|
516
|
+
const logoImg = document.createElement("img");
|
|
517
|
+
logoImg.src = `${this.client.getIframeOrigin()}/logo.svg`;
|
|
518
|
+
logoImg.alt = "";
|
|
519
|
+
logoImg.width = 20;
|
|
520
|
+
logoImg.height = 20;
|
|
521
|
+
logoImg.className = "yak-widget-icon";
|
|
522
|
+
iconBg.innerHTML = "";
|
|
523
|
+
iconBg.appendChild(logoImg);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
sendPendingPrompt() {
|
|
528
|
+
if (!this.pendingPrompt || !this.isReady)
|
|
529
|
+
return;
|
|
530
|
+
logger.debug("Sending pending prompt:", this.pendingPrompt);
|
|
531
|
+
this.client.sendPrompt(this.pendingPrompt);
|
|
532
|
+
this.pendingPrompt = null;
|
|
533
|
+
}
|
|
534
|
+
sendFocusIfOpen() {
|
|
535
|
+
if (this.isOpen && this.isReady) {
|
|
536
|
+
this.client.sendFocus();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
notifyMobileState() {
|
|
540
|
+
if (this.mobileQuery) {
|
|
541
|
+
this.notifyIframeFullscreen(this.mobileQuery.matches);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
notifyIframeFullscreen(isFullscreen) {
|
|
545
|
+
if (!this.iframe?.contentWindow)
|
|
546
|
+
return;
|
|
547
|
+
const msg = {
|
|
548
|
+
type: "yak:viewport",
|
|
549
|
+
payload: { fullscreen: isFullscreen },
|
|
550
|
+
};
|
|
551
|
+
this.iframe.contentWindow.postMessage(msg, this.client.getIframeOrigin());
|
|
552
|
+
}
|
|
553
|
+
notifyListeners() {
|
|
554
|
+
const state = this.getState();
|
|
555
|
+
for (const listener of this.stateListeners) {
|
|
556
|
+
try {
|
|
557
|
+
listener(state);
|
|
558
|
+
}
|
|
559
|
+
catch (err) {
|
|
560
|
+
logger.warn("Error in state listener:", err);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,5 +6,7 @@ export { EMBED_PROTOCOL_VERSION } from "./version.js";
|
|
|
6
6
|
export type { EmbedProtocolVersion } from "./version.js";
|
|
7
7
|
export { YakClient } from "./client.js";
|
|
8
8
|
export type { YakClientConfig } from "./client.js";
|
|
9
|
+
export { YakEmbed } from "./embed.js";
|
|
10
|
+
export type { YakEmbedConfig, YakEmbedState, YakEmbedStateListener, TriggerButtonConfig, } from "./embed.js";
|
|
9
11
|
export { enableYakLogging, disableYakLogging, isYakLoggingEnabled, logger } from "./logger.js";
|
|
10
12
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AAGjC,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,YAAY,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAGzD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnD,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AAGjC,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,YAAY,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAGzD,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,YAAY,EACV,cAAc,EACd,aAAa,EACb,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -7,5 +7,7 @@ export * from "./types/tools.js";
|
|
|
7
7
|
export { EMBED_PROTOCOL_VERSION } from "./version.js";
|
|
8
8
|
// Public client API
|
|
9
9
|
export { YakClient } from "./client.js";
|
|
10
|
+
// Embed (DOM rendering layer)
|
|
11
|
+
export { YakEmbed } from "./embed.js";
|
|
10
12
|
// Logging utilities
|
|
11
13
|
export { enableYakLogging, disableYakLogging, isYakLoggingEnabled, logger } from "./logger.js";
|