@haikit/client 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,6 +20,10 @@ property, so override what you like.
20
20
 
21
21
  ## The default UI
22
22
 
23
+ The [default UI guide](https://github.com/wfoxd/haikit/blob/main/docs/default-ui.md)
24
+ covers it in full: every option, what each part of the screen does, theming,
25
+ styling your surfaces, and building your own layout. In short:
26
+
23
27
  `mountChat` gives you a header with **New chat** and a **Context** button that
24
28
  shows how many tokens the model holds, a centred conversation, and a composer
25
29
  that grows as you type.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haikit/client",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "The haikit browser runtime: streams tool payloads into interactive UI surfaces. Plain ESM, no build step, no dependencies.",
5
5
  "keywords": [
6
6
  "haikit",
package/src/app.js CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  import { createChat } from "./index.js";
19
19
  import { renderTranscript, closeTranscript, h } from "./transcript.js";
20
- import { icon } from "./icons.js";
20
+ import { icon, mark, FAVICON } from "./icons.js";
21
21
 
22
22
  /** Below this width the drawer opens over the chat rather than beside it. */
23
23
  const WIDE = "(min-width: 900px)";
@@ -60,6 +60,28 @@ function setPin(theme) {
60
60
  pinning.ours = theme;
61
61
  }
62
62
 
63
+ /**
64
+ * A page without an icon of its own gets the haikit favicon while a chat is
65
+ * open on it. Every chat shares the one link; the last to close takes it away.
66
+ */
67
+ const favicon = { chats: 0, link: null };
68
+
69
+ function addFavicon() {
70
+ if (favicon.chats++ || document.querySelector('link[rel~="icon"]')) return;
71
+ const link = document.createElement("link");
72
+ link.rel = "icon";
73
+ link.type = "image/svg+xml";
74
+ link.href = FAVICON;
75
+ document.head.append(link);
76
+ favicon.link = link;
77
+ }
78
+
79
+ function dropFavicon() {
80
+ if (--favicon.chats) return;
81
+ favicon.link?.remove();
82
+ favicon.link = null;
83
+ }
84
+
63
85
  /**
64
86
  * @param {{
65
87
  * root: HTMLElement,
@@ -78,7 +100,7 @@ export function mountChat({
78
100
  root,
79
101
  registry,
80
102
  endpoint = "/hai",
81
- title = "hai",
103
+ title = "HaiKIT",
82
104
  subtitle = "",
83
105
  suggestions = [],
84
106
  inspector = true,
@@ -94,13 +116,14 @@ export function mountChat({
94
116
  // so a pin on the shell alone would not reach them.
95
117
  const pinned = theme === "light" || theme === "dark" ? { theme } : null;
96
118
  if (pinned) pin(pinned);
119
+ addFavicon();
97
120
 
98
121
  // ── shell ──────────────────────────────────────────────────────────
99
122
  const shell = h("div", "hai-shell");
100
123
 
101
124
  const top = h("header", "hai-top");
102
125
  const brand = h("div", "hai-brand");
103
- brand.append(icon("brand", "hai-icon hai-mark"), h("span", "hai-title", title));
126
+ brand.append(mark("small", "hai-logo hai-mark"), h("span", "hai-title", title));
104
127
  if (subtitle) brand.append(h("span", "hai-subtitle", subtitle));
105
128
 
106
129
  const modelEl = h("span", "hai-model");
@@ -256,7 +279,7 @@ export function mountChat({
256
279
  function emptyState() {
257
280
  const empty = h("div", "hai-empty");
258
281
  if (!emptyText && !suggestions.length) return empty;
259
- empty.append(icon("brand", "hai-icon hai-empty-mark"));
282
+ empty.append(mark("full", "hai-logo hai-empty-mark"));
260
283
  if (emptyText) empty.append(h("p", null, emptyText));
261
284
  if (suggestions.length) {
262
285
  const row = h("div", "hai-suggestions");
@@ -310,6 +333,7 @@ export function mountChat({
310
333
  shell.remove();
311
334
  releaseDrawer();
312
335
  if (pinned) unpin(pinned);
336
+ dropFavicon();
313
337
  return;
314
338
  }
315
339
 
package/src/hai.css CHANGED
@@ -41,6 +41,9 @@
41
41
  --hai-user-bg: light-dark(#e7eefb, #1e2835);
42
42
  --hai-code-bg: light-dark(#f3f5f8, #0a0d11);
43
43
  --hai-shadow: 0 1px 2px light-dark(rgb(15 23 42 / .06), rgb(0 0 0 / .35));
44
+ --hai-logo-ink: light-dark(#0f1115, #f3f2ee);
45
+ --hai-logo-paper: light-dark(#f3f2ee, #0f1115);
46
+ --hai-logo-hot: #ff5a1f;
44
47
 
45
48
  /* tints, derived — override the colour and these follow */
46
49
  --hai-line-soft: color-mix(in srgb, var(--hai-line) 60%, transparent);
@@ -79,6 +82,8 @@
79
82
  --hai-user-bg: #1e2835;
80
83
  --hai-code-bg: #0a0d11;
81
84
  --hai-shadow: 0 1px 2px rgb(0 0 0 / .35);
85
+ --hai-logo-ink: #f3f2ee;
86
+ --hai-logo-paper: #0f1115;
82
87
  }
83
88
  }
84
89
 
@@ -110,7 +115,12 @@
110
115
  background: var(--hai-panel);
111
116
  }
112
117
  .hai-brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
113
- .hai-mark { width: 22px; height: 22px; color: var(--hai-accent); }
118
+ .hai-logo { flex: none; }
119
+ .hai-logo-ink { fill: var(--hai-logo-ink); }
120
+ .hai-logo-paper { fill: var(--hai-logo-paper); }
121
+ .hai-logo-paper-dim { fill: var(--hai-logo-paper); opacity: .5; }
122
+ .hai-logo-hot { fill: var(--hai-logo-hot); }
123
+ .hai-mark { width: 20px; height: 20px; }
114
124
  .hai-title { font-size: 15px; font-weight: 650; letter-spacing: -.01em; white-space: nowrap; }
115
125
  .hai-subtitle {
116
126
  color: var(--hai-dim);
@@ -171,8 +181,11 @@
171
181
  }
172
182
  .hai-icon-btn:hover { background: var(--hai-hover); color: var(--hai-fg); }
173
183
 
174
- /* clips the drawer while it waits off the right edge */
175
- .hai-main { flex: 1; display: grid; grid-template-columns: minmax(0, 1fr); min-height: 0; position: relative; overflow: hidden; }
184
+ /* Clips the drawer while it waits off the right edge. `clip`, not just
185
+ `hidden`: a hidden box still scrolls, so bringing anything into view
186
+ centred, as find in page does, slid the whole chat sideways and left it
187
+ there. `hidden` stays for browsers without `clip`. */
188
+ .hai-main { flex: 1; display: grid; grid-template-columns: minmax(0, 1fr); min-height: 0; position: relative; overflow: hidden; overflow: clip; }
176
189
 
177
190
  /* the conversation is a centred column; the scrollbar stays at the edge */
178
191
  .hai-chat {
@@ -200,15 +213,7 @@
200
213
  /* ── empty state ────────────────────────────────────────────────────── */
201
214
  .hai-empty { color: var(--hai-dim); }
202
215
  .hai-empty p { margin: 0; max-width: 54ch; font-size: 15px; line-height: 1.6; }
203
- .hai-empty-mark {
204
- width: 28px;
205
- height: 28px;
206
- padding: 12px;
207
- margin-bottom: 18px;
208
- border-radius: 16px;
209
- background: var(--hai-accent-soft);
210
- color: var(--hai-accent);
211
- }
216
+ .hai-empty-mark { width: 44px; height: 44px; margin-bottom: 20px; }
212
217
  /* alone, it greets; after what init showed, it stays out of the way */
213
218
  .hai-empty:first-child {
214
219
  display: flex;
package/src/icons.js CHANGED
@@ -7,8 +7,6 @@
7
7
  */
8
8
 
9
9
  const PATHS = {
10
- // one line splitting in two: the dual channel
11
- brand: "M2.5 12H8c3.5 0 4.5-6 9-6h4.5M8 12c3.5 0 4.5 6 9 6h4.5",
12
10
  send: "M12 19V5M5.5 11.5 12 5l6.5 6.5",
13
11
  plus: "M12 5v14M5 12h14",
14
12
  panel: "M5 4h14a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2zM15 4v16",
@@ -22,6 +20,12 @@ const PATHS = {
22
20
 
23
21
  const SVG = "http://www.w3.org/2000/svg";
24
22
 
23
+ /** The haikit favicon, docs/haikit-logo/favicon.svg, as a data URL. Its dark
24
+ * tile reads on a light tab bar and a dark one alike. */
25
+ export const FAVICON = "data:image/svg+xml," + encodeURIComponent(
26
+ "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64' role='img' aria-label='HaiKIT'><title>HaiKIT</title><rect width='64' height='64' rx='14' fill='#0F1115'/><g transform='translate(8 8) scale(0.75)'><rect x='6' y='6' width='28' height='52' rx='8' fill='#F3F2EE'/><rect x='33' y='26' width='12' height='12' fill='#F3F2EE'/><rect x='44' y='6' width='14' height='52' rx='4' fill='#FF5A1F'/></g></svg>",
27
+ );
28
+
25
29
  /** An inline SVG icon, hidden from assistive technology: the text beside it, or
26
30
  * the button's label, says what it means. */
27
31
  export function icon(name, className = "hai-icon") {
@@ -39,3 +43,48 @@ export function icon(name, className = "hai-icon") {
39
43
  svg.append(path);
40
44
  return svg;
41
45
  }
46
+
47
+ // The haikit mark, as filled rects: [x, y, width, height, rx, part]. The parts
48
+ // are coloured by hai.css, so the mark reverses in the dark scheme. Below about
49
+ // 32px the full mark's inner bars blur, so small sizes get the simpler one.
50
+ const MARKS = {
51
+ full: {
52
+ viewBox: "7 8 50 48",
53
+ rects: [
54
+ [7, 8, 26, 48, 7, "ink"],
55
+ [12.5, 14.5, 15, 3, 1.5, "paper"],
56
+ [12.5, 20.5, 9, 3, 1.5, "paper-dim"],
57
+ [12.5, 43.5, 15, 7, 3.5, "hot"],
58
+ [32, 28, 14, 8, 0, "ink"],
59
+ [45, 8, 12, 15, 3, "hot"],
60
+ [45, 25, 12, 14, 3, "hot"],
61
+ [45, 41, 12, 15, 3, "hot"],
62
+ ],
63
+ },
64
+ small: {
65
+ viewBox: "6 6 52 52",
66
+ rects: [
67
+ [6, 6, 28, 52, 8, "ink"],
68
+ [33, 26, 12, 12, 0, "ink"],
69
+ [44, 6, 14, 52, 4, "hot"],
70
+ ],
71
+ },
72
+ };
73
+
74
+ /** The haikit logo mark, hidden from assistive technology like the icons. */
75
+ export function mark(size = "full", className = "hai-logo") {
76
+ const { viewBox, rects } = MARKS[size];
77
+ const svg = document.createElementNS(SVG, "svg");
78
+ svg.setAttribute("class", className);
79
+ svg.setAttribute("viewBox", viewBox);
80
+ svg.setAttribute("aria-hidden", "true");
81
+ for (const [x, y, width, height, rx, part] of rects) {
82
+ const rect = document.createElementNS(SVG, "rect");
83
+ for (const [k, v] of Object.entries({ x, y, width, height, rx })) {
84
+ if (v) rect.setAttribute(k, String(v));
85
+ }
86
+ rect.setAttribute("class", `hai-logo-${part}`);
87
+ svg.append(rect);
88
+ }
89
+ return svg;
90
+ }